fix: update type generation

This commit is contained in:
hypercross 2026-04-15 14:46:03 +08:00
parent 6eba70bb3b
commit 852a108c53
2 changed files with 59 additions and 22 deletions

View File

@ -846,15 +846,15 @@ describe('csvToModule - accessor-based output', () => {
});
describe('csvToModule - circular reference support', () => {
it('should emit accessor for self-referencing table', () => {
it('should emit accessor for self-referencing table without self-import', () => {
const csv = readFixture('self_ref.csv');
const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, 'self_ref.csv') });
expect(result.js).toContain("import _self_ref from './self_ref.csv'");
expect(result.js).not.toContain("import _self_ref from './self_ref.csv'");
expect(result.js).toContain('export default function getData()');
expect(result.js).toContain('_self_refLookup');
expect(result.js).toContain('_resolved = _raw;');
expect(result.js).toContain('_self_refLookup = new Map(_raw.map');
expect(result.js).toContain('parent: _self_refLookup.get(String(row.parent))');
});
@ -878,14 +878,14 @@ describe('csvToModule - circular reference support', () => {
const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, 'self_ref.csv') });
expect(result.js).toContain("import _self_ref from './self_ref.csv'");
expect(result.js).not.toContain("import _self_ref from './self_ref.csv'");
expect(result.js).toContain('_self_refLookup');
expect(result.js).toContain('export default function getData()');
expect(result.js).toContain('parent_info:');
expect(result.js).toContain('_self_refLookup.get(String(row.parent_info[0]))');
});
it('should emit accessor for self-referencing table with nested reference in union', () => {
it('should emit accessor for self-referencing table with nested reference in union with fallback', () => {
const csv = [
'id,name,ref_or_val',
'string,string,@self_ref | string',
@ -895,10 +895,11 @@ describe('csvToModule - circular reference support', () => {
const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, 'self_ref.csv') });
expect(result.js).toContain("import _self_ref from './self_ref.csv'");
expect(result.js).not.toContain("import _self_ref from './self_ref.csv'");
expect(result.js).toContain('_self_refLookup');
expect(result.js).toContain('export default function getData()');
expect(result.js).toContain('ref_or_val:');
expect(result.js).toContain('_self_refLookup.get(String(row.ref_or_val)) ?? row.ref_or_val');
});
it('should emit accessor for self-referencing table with nested reference array in tuple', () => {
@ -910,20 +911,21 @@ describe('csvToModule - circular reference support', () => {
const result = csvToModule(csv, { emitTypes: false, currentFilePath: path.join(fixturesDir, 'self_ref.csv') });
expect(result.js).toContain("import _self_ref from './self_ref.csv'");
expect(result.js).not.toContain("import _self_ref from './self_ref.csv'");
expect(result.js).toContain('_self_refLookup');
expect(result.js).toContain('export default function getData()');
expect(result.js).toContain('children:');
});
it('should generate correct type definition for self-referencing table', () => {
it('should generate correct type definition for self-referencing table using local singular type', () => {
const csv = readFixture('self_ref.csv');
const result = csvToModule(csv, { emitTypes: true, resourceName: 'nodes', currentFilePath: path.join(fixturesDir, 'self_ref.csv') });
expect(result.dts).toContain('declare function getData(): nodesTable');
expect(result.dts).toContain('Self_ref');
expect(result.dts).toContain("import type { Self_ref } from './self_ref.csv'");
expect(result.dts).toContain('readonly parent: Nodes');
expect(result.dts).not.toContain("import type { Self_ref } from './self_ref.csv'");
expect(result.dts).not.toContain('Self_ref');
});
it('should emit accessor for cross-referencing tables with array references', () => {
@ -950,4 +952,17 @@ describe('csvToModule - circular reference support', () => {
expect(result.js).toContain('_circular_bLookup');
expect(result.js).toContain('export default function getData()');
});
it('should generate union fallback with ?? for non-reference union members', () => {
const csv = [
'id,value',
'string,@users | string',
'1,1',
'2,unknown',
].join('\n');
const result = csvToModule(csv, { emitTypes: false });
expect(result.js).toContain('?? row.value');
});
});

View File

@ -538,12 +538,22 @@ function generateTypeDefinition(
hasRefs?: boolean
): string {
const typeName = resourceName ? `${resourceName}Table` : 'Table';
const currentTableName = currentFilePath
? path.basename(currentFilePath, path.extname(currentFilePath))
: undefined;
const singularType = resourceName
? resourceName.charAt(0).toUpperCase() + resourceName.slice(1)
: `${typeName}[number]`;
// Generate import statements for referenced tables
const imports: string[] = [];
const resourceNames = new Map<string, string>();
references.forEach(tableName => {
if (tableName === currentTableName) {
resourceNames.set(tableName, singularType);
return;
}
// Convert table name to type name by capitalizing
const typeBase = tableName.charAt(0).toUpperCase() + tableName.slice(1);
resourceNames.set(tableName, typeBase);
@ -551,10 +561,8 @@ function generateTypeDefinition(
// Generate import path based on current file path
let importPath: string;
if (currentFilePath) {
// Both files are in the same directory, use relative path
importPath = `./${tableName}.csv`;
} else {
// Fallback for unknown path
importPath = `../${tableName}.csv`;
}
imports.push(`import type { ${typeBase} } from '${importPath}';`);
@ -793,15 +801,17 @@ function generateSchemaResolutionCode(
case 'union': {
const refMembers = schema.members.filter(m => hasNestedReferences(m));
const nonRefMembers = schema.members.filter(m => !hasNestedReferences(m));
const parts: string[] = [];
const resolveParts: string[] = [];
for (const member of refMembers) {
const resolveCode = generateSchemaResolutionCode(member, valueExpr, lookupVar, pkField);
parts.push(resolveCode);
resolveParts.push(resolveCode);
}
if (nonRefMembers.length > 0) {
parts.push(valueExpr);
resolveParts.push(valueExpr);
}
return parts[0] || valueExpr;
if (resolveParts.length === 0) return valueExpr;
if (resolveParts.length === 1) return resolveParts[0];
return `(${resolveParts.join(' ?? ')})`;
}
default:
return valueExpr;
@ -834,14 +844,26 @@ export function csvToModule(
const lookupInits: string[] = [];
const lookupVarMap = new Map<string, string>();
const currentTableName = options.currentFilePath
? path.basename(options.currentFilePath, path.extname(options.currentFilePath))
: undefined;
const uniqueTables = new Set(result.referenceFields.map(f => f.tableName));
uniqueTables.forEach(tableName => {
const lookupVar = `_${tableName}Lookup`;
lookupVarMap.set(tableName, lookupVar);
if (tableName === currentTableName) {
lookupInits.push(
`const ${lookupVar} = new Map(_raw.map(p => [String(p.${defaultPrimaryKey}), p]));`
);
} else {
const varName = `_${tableName}`;
lookupVarMap.set(tableName, `_${tableName}Lookup`);
imports.push(`import ${varName} from './${tableName}.csv';`);
lookupInits.push(
`const _${tableName}Lookup = new Map(${varName}().map(p => [String(p.${defaultPrimaryKey}), p]));`
`const ${lookupVar} = new Map(${varName}().map(p => [String(p.${defaultPrimaryKey}), p]));`
);
}
});
const lookupVar = (tableName: string) => lookupVarMap.get(tableName)!;