这是indexloc提供的服务,不要输入任何密码
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, expect, it } from 'vitest';

import { TypeScriptRenderer } from '../renderer';

// Minimal local BiMap for tests
class LocalBiMap<Key, Value> {
private map = new Map<Key, Value>();
private reverse = new Map<Value, Key>();
get(key: Key) {
return this.map.get(key);
}
getKey(value: Value) {
return this.reverse.get(value);
}
set(key: Key, value: Value) {
this.map.set(key, value);
this.reverse.set(value, key);
return this;
}
hasValue(value: Value) {
return this.reverse.has(value);
}
}

describe('TypeScriptRenderer - Placeholder Replacement', () => {
it('should replace placeholders in external symbol references', () => {
const renderer = new TypeScriptRenderer();

const symbolId = 1;
const symbol = {
external: 'zod',
id: symbolId,
kind: undefined,
meta: { category: 'external', resource: 'zod.z' },
name: 'z',
placeholder: '_heyapi_1_',
};

const project = {
symbolIdToFiles: () => [],
symbols: new Map([[symbolId, symbol]]),
} as any;

const file: any = {
resolvedNames: new LocalBiMap<number, string>(),
};

// Simulate rendering content with a placeholder
const content = `export const schema = ${symbol.placeholder}.object({});`;

// This is what renderFile does internally
const processed = content.replace(/_heyapi_(\d+)_/g, (match) => {
const id = Number.parseInt(match.slice('_heyapi_'.length, -1), 10);
const sym = project.symbols.get(id);
const result = renderer['replacerFn']({ file, project, symbol: sym });
return result || match;
});

expect(processed).toBe('export const schema = z.object({});');
});

it('should handle stub symbols that are later registered', () => {
const renderer = new TypeScriptRenderer();

// First, create a stub (symbol without name)
const stubId = 1;
const stub = {
exportFrom: [],
external: 'zod',
id: stubId,
meta: { category: 'external', resource: 'zod.z' },
placeholder: '_heyapi_1_',
// Note: no 'name' property!
};

const project = {
symbolIdToFiles: () => [],
symbols: new Map([[stubId, stub]]),
} as any;

const file: any = {
resolvedNames: new LocalBiMap<number, string>(),
};

// Try to replace placeholder with stub
const result = renderer['replacerFn']({ file, project, symbol: stub });

// With the fix: replacerFn now derives the name from the resource
// even if stub.name is undefined
expect(result).toBe('z');
});

it('should handle external symbols without names by using resource', () => {
const renderer = new TypeScriptRenderer();

const symbolId = 1;
const symbol = {
exportFrom: [],
external: 'zod',
id: symbolId,
meta: { category: 'external', resource: 'zod.z' },
placeholder: '_heyapi_1_',
// Note: no 'name' property, but has 'external' and 'meta.resource'
};

const project = {
symbolIdToFiles: () => [],
symbols: new Map([[symbolId, symbol]]),
} as any;

const file: any = {
resolvedNames: new LocalBiMap<number, string>(),
};

// This test documents the expected behavior:
// When a symbol has no name but has external+resource, we should derive the name
const result = renderer['replacerFn']({ file, project, symbol });

// With the fix: derives the name from the resource 'zod.z' → 'z'
expect(result).toBe('z');
});
});
16 changes: 14 additions & 2 deletions packages/openapi-ts/src/generate/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,10 +475,22 @@ export class TypeScriptRenderer implements Renderer {
if (!symbol) return;
const cached = file.resolvedNames.get(symbol.id);
if (cached) return cached;
if (!symbol.name) return;

// Handle symbols without a name by deriving it from the resource
let symbolName = symbol.name;
if (!symbolName && symbol.meta?.resource) {
// For external symbols like 'zod.z', extract the last part as the name
const resource = symbol.meta.resource;
if (typeof resource === 'string') {
const parts = resource.split('.');
symbolName = parts[parts.length - 1];
}
}

if (!symbolName) return;
const [symbolFile] = project.symbolIdToFiles(symbol.id);
const symbolFileResolvedName = symbolFile?.resolvedNames.get(symbol.id);
let name = ensureValidIdentifier(symbolFileResolvedName ?? symbol.name);
let name = ensureValidIdentifier(symbolFileResolvedName ?? symbolName);
const conflictId = file.resolvedNames.getKey(name);
if (conflictId !== undefined) {
const conflictSymbol = project.symbols.get(conflictId);
Expand Down