Skip to content
Draft
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
67 changes: 54 additions & 13 deletions packages/core/src/generators/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,15 @@ const generateDependency = ({
let importString = '';

// generate namespace import string
// For zod imports (relative paths), use the dependency path directly
const isZodImportForNamespace =
dependency.startsWith('./') || dependency.startsWith('../');
const namespaceImportPath =
isZodImportForNamespace && dependency === key
? dependency // Use the relative path directly for Zod files
: dependency;
const namespaceImportString = namespaceImportDep
? `import * as ${namespaceImportDep.name} from '${dependency}';`
? `import * as ${namespaceImportDep.name} from '${namespaceImportPath}';`
: '';

if (namespaceImportString) {
Expand All @@ -177,11 +184,18 @@ const generateDependency = ({
importString += `${namespaceImportString}\n`;
}

// Check if dependency is a relative path (starts with './' or '../') - this indicates a Zod file import
// In this case, use the dependency directly as the import path (dependency should equal key for zod imports)
const isZodImport =
dependency.startsWith('./') || dependency.startsWith('../');
const importPath =
isZodImport && dependency === key
? dependency // Use the relative path directly for Zod files
: `${dependency}${key !== 'default' && specsName[key] ? `/${specsName[key]}` : ''}`;

importString += `import ${onlyTypes ? 'type ' : ''}${
defaultDep ? `${defaultDep.name}${depsString ? ',' : ''}` : ''
}${depsString ? `{\n ${depsString}\n}` : ''} from '${dependency}${
key !== 'default' && specsName[key] ? `/${specsName[key]}` : ''
}';`;
}${depsString ? `{\n ${depsString}\n}` : ''} from '${importPath}';`;

return importString;
};
Expand All @@ -201,12 +215,21 @@ export const addDependency = ({
hasSchemaDir: boolean;
isAllowSyntheticDefaultImports: boolean;
}) => {
const toAdds = exports.filter((e) => {
const searchWords = [e.alias, e.name].filter((p) => p?.length).join('|');
const pattern = new RegExp(`\\b(${searchWords})\\b`, 'g');

return implementation.match(pattern);
});
// For Zod imports (relative paths), always include all exports
// since they are needed for types and schemas even if not explicitly used in runtime code
const isZodImport =
dependency.startsWith('./') || dependency.startsWith('../');

const toAdds = isZodImport
? exports // Include all exports for Zod imports
: exports.filter((e) => {
const searchWords = [e.alias, e.name]
.filter((p) => p?.length)
.join('|');
const pattern = new RegExp(`\\b(${searchWords})\\b`, 'g');

return implementation.match(pattern);
});

if (toAdds.length === 0) {
return;
Expand All @@ -215,7 +238,15 @@ export const addDependency = ({
const groupedBySpecKey = toAdds.reduce<
Record<string, { types: GeneratorImport[]; values: GeneratorImport[] }>
>((acc, dep) => {
const key = hasSchemaDir && dep.specKey ? dep.specKey : 'default';
// If specKey is a relative path (starts with './' or '../'), use it directly
// Otherwise, use the standard logic with hasSchemaDir
const key =
dep.specKey &&
(dep.specKey.startsWith('./') || dep.specKey.startsWith('../'))
? dep.specKey
: hasSchemaDir && dep.specKey
? dep.specKey
: 'default';

if (
dep.values &&
Expand Down Expand Up @@ -336,7 +367,17 @@ export const generateVerbImports = ({
? [{ name: prop.schema.name }]
: [],
),
...(queryParams ? [{ name: queryParams.schema.name }] : []),
...(headers ? [{ name: headers.schema.name }] : []),
// Use queryParams.schema.imports if available (for zod model style), otherwise use schema.name
...(queryParams
? queryParams.schema.imports && queryParams.schema.imports.length > 0
? queryParams.schema.imports
: [{ name: queryParams.schema.name }]
: []),
// Use headers.schema.imports if available, otherwise use schema.name
...(headers
? headers.schema.imports && headers.schema.imports.length > 0
? headers.schema.imports
: [{ name: headers.schema.name }]
: []),
...params.flatMap<GeneratorImport>(({ imports }) => imports),
];
9 changes: 9 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type NormalizedOutputOptions = {
unionAddMissingProperties: boolean;
optionsParamRequired: boolean;
propertySortOrder: PropertySortOrder;
modelStyle: ModelStyle;
};

export type NormalizedParamsSerializerOptions = {
Expand Down Expand Up @@ -206,6 +207,13 @@ export const EnumGeneration = {
export type EnumGeneration =
(typeof EnumGeneration)[keyof typeof EnumGeneration];

export const ModelStyle = {
TYPESCRIPT: 'typescript',
ZOD: 'zod',
} as const;

export type ModelStyle = (typeof ModelStyle)[keyof typeof ModelStyle];

export type OutputOptions = {
workspace?: string;
target: string;
Expand All @@ -232,6 +240,7 @@ export type OutputOptions = {
unionAddMissingProperties?: boolean;
optionsParamRequired?: boolean;
propertySortOrder?: PropertySortOrder;
modelStyle?: ModelStyle;
};

export type SwaggerParserOptions = Omit<SwaggerParser.Options, 'validate'> & {
Expand Down
90 changes: 80 additions & 10 deletions packages/core/src/writers/generate-imports-for-builder.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,90 @@
import { uniqueBy } from 'remeda';

import type { GeneratorImport, NormalizedOutputOptions } from '../types';
import {
ModelStyle,
type GeneratorImport,
type NormalizedOutputOptions,
} from '../types';
import { conventionName, upath } from '../utils';

export const generateImportsForBuilder = (
output: NormalizedOutputOptions,
imports: GeneratorImport[],
relativeSchemasPath: string,
) => {
return output.schemas && !output.indexFiles
? uniqueBy(imports, (x) => x.name).map((i) => {
const name = conventionName(i.name, output.namingConvention);
return {
exports: [i],
dependency: upath.joinSafe(relativeSchemasPath, name),
};
})
: [{ exports: imports, dependency: relativeSchemasPath }];
// Separate Zod imports (with relative paths in specKey) from regular imports
const zodImports: GeneratorImport[] = [];
const regularImports: GeneratorImport[] = [];

imports.forEach((imp) => {
// Check if specKey is a relative path (starts with './' or '../') - this indicates a Zod file import
if (
imp.specKey &&
(imp.specKey.startsWith('./') || imp.specKey.startsWith('../'))
) {
zodImports.push(imp);
} else {
regularImports.push(imp);
}
});

// For zod model style, only generate Zod imports, skip regular schema imports
if (output.modelStyle === ModelStyle.ZOD) {
// Group Zod imports by their specKey (path to zod file)
const zodImportsByPath = zodImports.reduce<
Record<string, GeneratorImport[]>
>((acc, imp) => {
const key = imp.specKey || '';
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(imp);
return acc;
}, {});

// Generate imports for each Zod file path
const zodImportsResult = Object.entries(zodImportsByPath).map(
([path, exps]) => ({
exports: exps,
dependency: path, // Use the relative path directly
}),
);

return zodImportsResult;
}

// Generate regular imports from schemas (for non-zod model style)
const regularImportsResult =
output.schemas && !output.indexFiles
? uniqueBy(regularImports, (x) => x.name).map((i) => {
const name = conventionName(i.name, output.namingConvention);
return {
exports: [i],
dependency: upath.joinSafe(relativeSchemasPath, name),
};
})
: [{ exports: regularImports, dependency: relativeSchemasPath }];

// Group Zod imports by their specKey (path to zod file)
const zodImportsByPath = zodImports.reduce<Record<string, GeneratorImport[]>>(
(acc, imp) => {
const key = imp.specKey || '';
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(imp);
return acc;
},
{},
);

// Generate imports for each Zod file path
const zodImportsResult = Object.entries(zodImportsByPath).map(
([path, exps]) => ({
exports: exps,
dependency: path, // Use the relative path directly
}),
);

return [...regularImportsResult, ...zodImportsResult];
};
7 changes: 4 additions & 3 deletions packages/core/src/writers/single-mode.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs-extra';

import { generateModelsInline, generateMutatorImports } from '../generators';
import type { WriteModeProps } from '../types';
import { ModelStyle, type WriteModeProps } from '../types';
import {
conventionName,
getFileInfo,
Expand Down Expand Up @@ -75,7 +75,7 @@ export const writeSingleMode = async ({
isAllowSyntheticDefaultImports,
hasGlobalMutator: !!output.override.mutator,
hasTagsMutator: Object.values(output.override.tags).some(
(tag) => !!tag.mutator,
(tag) => !!tag?.mutator,
),
hasParamsSerializerOptions: !!output.override.paramsSerializerOptions,
packageJson: output.packageJson,
Expand Down Expand Up @@ -130,7 +130,8 @@ export const writeSingleMode = async ({
data += '\n';
}

if (!output.schemas && needSchema) {
// Don't generate TypeScript schemas for zod model style as we use zod types instead
if (!output.schemas && needSchema && output.modelStyle !== ModelStyle.ZOD) {
data += generateModelsInline(builder.schemas);
}

Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/writers/split-mode.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs-extra';

import { generateModelsInline, generateMutatorImports } from '../generators';
import { OutputClient, type WriteModeProps } from '../types';
import { ModelStyle, OutputClient, type WriteModeProps } from '../types';
import {
conventionName,
getFileInfo,
Expand Down Expand Up @@ -73,7 +73,7 @@ export const writeSplitMode = async ({
isAllowSyntheticDefaultImports,
hasGlobalMutator: !!output.override.mutator,
hasTagsMutator: Object.values(output.override.tags).some(
(tag) => !!tag.mutator,
(tag) => !!tag?.mutator,
),
hasParamsSerializerOptions: !!output.override.paramsSerializerOptions,
packageJson: output.packageJson,
Expand All @@ -99,7 +99,8 @@ export const writeSplitMode = async ({
? undefined
: upath.join(dirname, filename + '.schemas' + extension);

if (schemasPath && needSchema) {
// Don't generate TypeScript schemas for zod model style as we use zod types instead
if (schemasPath && needSchema && output.modelStyle !== ModelStyle.ZOD) {
const schemasData = header + generateModelsInline(builder.schemas);

await fs.outputFile(
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/writers/split-tags-mode.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import fs from 'fs-extra';

import { generateModelsInline, generateMutatorImports } from '../generators';
import { OutputClient, type WriteModeProps } from '../types';
import { ModelStyle, OutputClient, type WriteModeProps } from '../types';
import {
camel,
getFileInfo,
isFunction,
isSyntheticDefaultImportsAllow,
kebab,
pascal,
upath,
} from '../utils';
Expand Down Expand Up @@ -113,7 +114,8 @@ export const writeSplitTagsMode = async ({
? undefined
: upath.join(dirname, filename + '.schemas' + extension);

if (schemasPath && needSchema) {
// Don't generate TypeScript schemas for zod model style as we use zod types instead
if (schemasPath && needSchema && output.modelStyle !== ModelStyle.ZOD) {
const schemasData = header + generateModelsInline(builder.schemas);

await fs.outputFile(schemasPath, schemasData);
Expand Down Expand Up @@ -170,6 +172,9 @@ export const writeSplitTagsMode = async ({
implementationData += '\n';
}

// Note: zod imports are already added in generateTargetForTags,
// so we don't need to add them here again

implementationData += `\n${implementation}`;
mockData += `\n${implementationMock}`;

Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/writers/tags-mode.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs-extra';

import { generateModelsInline, generateMutatorImports } from '../generators';
import type { WriteModeProps } from '../types';
import { ModelStyle, type WriteModeProps } from '../types';
import {
camel,
getFileInfo,
Expand Down Expand Up @@ -103,7 +103,8 @@ export const writeTagsMode = async ({
? undefined
: upath.join(dirname, filename + '.schemas' + extension);

if (schemasPath && needSchema) {
// Don't generate TypeScript schemas for zod model style as we use zod types instead
if (schemasPath && needSchema && output.modelStyle !== ModelStyle.ZOD) {
const schemasData = header + generateModelsInline(builder.schemas);

await fs.outputFile(schemasPath, schemasData);
Expand Down Expand Up @@ -147,6 +148,9 @@ export const writeTagsMode = async ({
data += '\n';
}

// Note: zod imports are already added in generateTargetForTags,
// so we don't need to add them here again

data += implementation;

if (output.mock) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/writers/target-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type GeneratorOperation,
type GeneratorTarget,
type GeneratorTargetFull,
ModelStyle,
type NormalizedOutputOptions,
OutputClient,
type WriteSpecsBuilder,
Expand Down
2 changes: 1 addition & 1 deletion packages/orval/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export const generateOperations = (
paramsSerializer: verbOption.paramsSerializer,
operationName: verbOption.operationName,
fetchReviver: verbOption.fetchReviver,
};
} as any;

return acc;
},
Expand Down
2 changes: 2 additions & 0 deletions packages/orval/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
type NormalizedQueryOptions,
type OperationOptions,
type OptionsExport,
ModelStyle,
OutputClient,
OutputHttpClient,
OutputMode,
Expand Down Expand Up @@ -369,6 +370,7 @@ export const normalizeOptions = async (
optionsParamRequired: outputOptions.optionsParamRequired ?? false,
propertySortOrder:
outputOptions.propertySortOrder ?? PropertySortOrder.SPECIFICATION,
modelStyle: outputOptions.modelStyle ?? ModelStyle.TYPESCRIPT,
},
hooks: options.hooks ? normalizeHooks(options.hooks) : {},
};
Expand Down
Loading