From 206f886ed4ad27e60dfac63af7cc4cada7cef8af Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:02:59 +0800 Subject: [PATCH] update router config --- .../@orpc/contract.gen.ts | 12 +- .../custom-group-key/@orpc/contract.gen.ts | 7 +- .../custom-router-name/@orpc/contract.gen.ts | 12 +- .../plugins/zod/default/@orpc/contract.gen.ts | 12 +- .../@orpc/contract.gen.ts | 20 +-- .../custom-group-key/@orpc/contract.gen.ts | 17 +-- .../custom-router-name/@orpc/contract.gen.ts | 20 +-- .../plugins/zod/default/@orpc/contract.gen.ts | 20 +-- .../@orpc/contract.gen.ts | 20 +-- .../custom-group-key/@orpc/contract.gen.ts | 17 +-- .../custom-router-name/@orpc/contract.gen.ts | 20 +-- .../plugins/zod/default/@orpc/contract.gen.ts | 20 +-- .../src/plugins/@orpc/contract/config.ts | 113 +++++++-------- .../src/plugins/@orpc/contract/plugin.ts | 120 +++++++++++---- .../src/plugins/@orpc/contract/types.d.ts | 137 +++++++++++++++--- 15 files changed, 345 insertions(+), 222 deletions(-) diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts index e50db16fb..3d28a965e 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts @@ -47,14 +47,10 @@ export const fooBarPutRpc = base.route({ }).output(zFooBarPutResponse); export const router = { - foo: { - get: getFooRpc, - fooPost: fooPostRpc, - fooPut: fooPutRpc, - getBar: getFooBarRpc, - fooBarPost: fooBarPostRpc, - fooBarPut: fooBarPutRpc - } + getFoo: getFooRpc, + post: fooBarPostRpc, + put: fooBarPutRpc, + getFooBar: getFooBarRpc }; export type Router = typeof router; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts index 66da05dce..a5d0c632e 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts @@ -47,9 +47,10 @@ export const fooBarPutContract = base.route({ }).output(zFooBarPutResponse); export const router = { - get: { getFoo: getFooContract, getFooBar: getFooBarContract }, - post: { foo: fooPostContract, fooBar: fooBarPostContract }, - put: { foo: fooPutContract, fooBar: fooBarPutContract } + getFoo: getFooContract, + post: fooBarPostContract, + put: fooBarPutContract, + getFooBar: getFooBarContract }; export type Router = typeof router; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts index 5c0a1edb5..6e763c1f8 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts @@ -47,14 +47,10 @@ export const fooBarPutContract = base.route({ }).output(zFooBarPutResponse); export const contract = { - foo: { - get: getFooContract, - fooPost: fooPostContract, - fooPut: fooPutContract, - getBar: getFooBarContract, - fooBarPost: fooBarPostContract, - fooBarPut: fooBarPutContract - } + getFoo: getFooContract, + post: fooBarPostContract, + put: fooBarPutContract, + getFooBar: getFooBarContract }; export type Contract = typeof contract; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/default/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/default/@orpc/contract.gen.ts index 39ec128a7..a5d0c632e 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/default/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/zod/default/@orpc/contract.gen.ts @@ -47,14 +47,10 @@ export const fooBarPutContract = base.route({ }).output(zFooBarPutResponse); export const router = { - foo: { - get: getFooContract, - fooPost: fooPostContract, - fooPut: fooPutContract, - getBar: getFooBarContract, - fooBarPost: fooBarPostContract, - fooBarPut: fooBarPutContract - } + getFoo: getFooContract, + post: fooBarPostContract, + put: fooBarPutContract, + getFooBar: getFooBarContract }; export type Router = typeof router; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts index 478d4ae0c..7df32091a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts @@ -97,18 +97,14 @@ export const getPostByIdRpc = base.route({ }).input(zGetPostByIdData).output(zGetPostByIdResponse); export const router = { - users: { - get: getUsersRpc, - create: createUserRpc, - delete: deleteUserRpc, - getById: getUserByIdRpc, - update: updateUserRpc - }, - posts: { - get: getPostsRpc, - create: createPostRpc, - getById: getPostByIdRpc - } + getUsers: getUsersRpc, + createUser: createUserRpc, + deleteUser: deleteUserRpc, + getUserById: getUserByIdRpc, + updateUser: updateUserRpc, + getPosts: getPostsRpc, + createPost: createPostRpc, + getPostById: getPostByIdRpc }; export type Router = typeof router; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts index 4d1d66ee1..8adb5e83c 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts @@ -97,15 +97,14 @@ export const getPostByIdContract = base.route({ }).input(zGetPostByIdData).output(zGetPostByIdResponse); export const router = { - get: { - getUsers: getUsersContract, - getUserById: getUserByIdContract, - getPosts: getPostsContract, - getPostById: getPostByIdContract - }, - post: { createUser: createUserContract, create: createPostContract }, - delete: { deleteUser: deleteUserContract }, - put: { updateUser: updateUserContract } + getUsers: getUsersContract, + createUser: createUserContract, + deleteUser: deleteUserContract, + getUserById: getUserByIdContract, + updateUser: updateUserContract, + getPosts: getPostsContract, + createPost: createPostContract, + getPostById: getPostByIdContract }; export type Router = typeof router; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts index 8192cd9e4..e2ae202e6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts @@ -97,18 +97,14 @@ export const getPostByIdContract = base.route({ }).input(zGetPostByIdData).output(zGetPostByIdResponse); export const contract = { - users: { - get: getUsersContract, - create: createUserContract, - delete: deleteUserContract, - getById: getUserByIdContract, - update: updateUserContract - }, - posts: { - get: getPostsContract, - create: createPostContract, - getById: getPostByIdContract - } + getUsers: getUsersContract, + createUser: createUserContract, + deleteUser: deleteUserContract, + getUserById: getUserByIdContract, + updateUser: updateUserContract, + getPosts: getPostsContract, + createPost: createPostContract, + getPostById: getPostByIdContract }; export type Contract = typeof contract; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/default/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/default/@orpc/contract.gen.ts index ad0e6c451..8adb5e83c 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/default/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/zod/default/@orpc/contract.gen.ts @@ -97,18 +97,14 @@ export const getPostByIdContract = base.route({ }).input(zGetPostByIdData).output(zGetPostByIdResponse); export const router = { - users: { - get: getUsersContract, - create: createUserContract, - delete: deleteUserContract, - getById: getUserByIdContract, - update: updateUserContract - }, - posts: { - get: getPostsContract, - create: createPostContract, - getById: getPostByIdContract - } + getUsers: getUsersContract, + createUser: createUserContract, + deleteUser: deleteUserContract, + getUserById: getUserByIdContract, + updateUser: updateUserContract, + getPosts: getPostsContract, + createPost: createPostContract, + getPostById: getPostByIdContract }; export type Router = typeof router; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts index 478d4ae0c..7df32091a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-contract-name/@orpc/contract.gen.ts @@ -97,18 +97,14 @@ export const getPostByIdRpc = base.route({ }).input(zGetPostByIdData).output(zGetPostByIdResponse); export const router = { - users: { - get: getUsersRpc, - create: createUserRpc, - delete: deleteUserRpc, - getById: getUserByIdRpc, - update: updateUserRpc - }, - posts: { - get: getPostsRpc, - create: createPostRpc, - getById: getPostByIdRpc - } + getUsers: getUsersRpc, + createUser: createUserRpc, + deleteUser: deleteUserRpc, + getUserById: getUserByIdRpc, + updateUser: updateUserRpc, + getPosts: getPostsRpc, + createPost: createPostRpc, + getPostById: getPostByIdRpc }; export type Router = typeof router; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts index 4d1d66ee1..8adb5e83c 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-group-key/@orpc/contract.gen.ts @@ -97,15 +97,14 @@ export const getPostByIdContract = base.route({ }).input(zGetPostByIdData).output(zGetPostByIdResponse); export const router = { - get: { - getUsers: getUsersContract, - getUserById: getUserByIdContract, - getPosts: getPostsContract, - getPostById: getPostByIdContract - }, - post: { createUser: createUserContract, create: createPostContract }, - delete: { deleteUser: deleteUserContract }, - put: { updateUser: updateUserContract } + getUsers: getUsersContract, + createUser: createUserContract, + deleteUser: deleteUserContract, + getUserById: getUserByIdContract, + updateUser: updateUserContract, + getPosts: getPostsContract, + createPost: createPostContract, + getPostById: getPostByIdContract }; export type Router = typeof router; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts index 8192cd9e4..e2ae202e6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/custom-router-name/@orpc/contract.gen.ts @@ -97,18 +97,14 @@ export const getPostByIdContract = base.route({ }).input(zGetPostByIdData).output(zGetPostByIdResponse); export const contract = { - users: { - get: getUsersContract, - create: createUserContract, - delete: deleteUserContract, - getById: getUserByIdContract, - update: updateUserContract - }, - posts: { - get: getPostsContract, - create: createPostContract, - getById: getPostByIdContract - } + getUsers: getUsersContract, + createUser: createUserContract, + deleteUser: deleteUserContract, + getUserById: getUserByIdContract, + updateUser: updateUserContract, + getPosts: getPostsContract, + createPost: createPostContract, + getPostById: getPostByIdContract }; export type Contract = typeof contract; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/default/@orpc/contract.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/default/@orpc/contract.gen.ts index ad0e6c451..8adb5e83c 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/default/@orpc/contract.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/zod/default/@orpc/contract.gen.ts @@ -97,18 +97,14 @@ export const getPostByIdContract = base.route({ }).input(zGetPostByIdData).output(zGetPostByIdResponse); export const router = { - users: { - get: getUsersContract, - create: createUserContract, - delete: deleteUserContract, - getById: getUserByIdContract, - update: updateUserContract - }, - posts: { - get: getPostsContract, - create: createPostContract, - getById: getPostByIdContract - } + getUsers: getUsersContract, + createUser: createUserContract, + deleteUser: deleteUserContract, + getUserById: getUserByIdContract, + updateUser: updateUserContract, + getPosts: getPostsContract, + createPost: createPostContract, + getPostById: getPostByIdContract }; export type Router = typeof router; diff --git a/packages/openapi-ts/src/plugins/@orpc/contract/config.ts b/packages/openapi-ts/src/plugins/@orpc/contract/config.ts index 6dab9d7ca..d9baec31e 100644 --- a/packages/openapi-ts/src/plugins/@orpc/contract/config.ts +++ b/packages/openapi-ts/src/plugins/@orpc/contract/config.ts @@ -1,81 +1,78 @@ -import type { IR } from '~/ir/types'; +import type { OperationsStrategy } from '~/openApi/shared/locations'; import { definePluginConfig } from '~/plugins/shared/utils/config'; -import { resolveNaming, toCase } from '~/utils/naming'; +import type { PluginContext } from '~/plugins/types'; +import { resolveNaming } from '~/utils/naming'; import { handler } from './plugin'; -import type { OrpcContractPlugin } from './types'; +import type { + OrpcContractPlugin, + RouterConfig, + UserRouterConfig, +} from './types'; -// Default: extract first path segment and convert to camelCase -// "/chat-messages/{id}" → "chatMessages" -function defaultGroupKeyBuilder(operation: IR.OperationObject): string { - const segment = operation.path.split('/').filter(Boolean)[0] || 'common'; - return toCase(segment, 'camelCase'); -} - -// Build patterns from segment name (camelCase group key) -// "chatMessages" → ["ChatMessages", "ChatMessage"] -function buildGroupPatterns(groupKey: string): string[] { - const patterns: string[] = []; - const pascalKey = toCase(groupKey, 'PascalCase'); - patterns.push(pascalKey); - - // Singular form: "ChatMessages" → "ChatMessage" - if (pascalKey.endsWith('s') && !pascalKey.endsWith('ss')) { - patterns.push(pascalKey.slice(0, -1)); - } - - return patterns; -} - -// Default: simplify operationId by removing redundant group-based patterns -// e.g., "sendChatMessage" with groupKey "chatMessages" → "send" -// e.g., "getConversationsList" with groupKey "conversations" → "getList" -function defaultOperationKeyBuilder( - operationId: string, - groupKey: string, -): string { - const patternsToRemove = buildGroupPatterns(groupKey); - - let simplified = operationId; - - // Remove patterns iteratively - for (const pattern of patternsToRemove) { - const regex = new RegExp(pattern, 'g'); - const result = simplified.replace(regex, ''); - if (result !== simplified && result.length > 0) { - simplified = result; - } +function resolveRouter( + input: OperationsStrategy | UserRouterConfig | undefined, + context: PluginContext, +): RouterConfig { + if (!input || typeof input === 'string' || typeof input === 'function') { + input = { strategy: input }; } - // Ensure first char is lowercase - simplified = simplified.charAt(0).toLowerCase() + simplified.slice(1); - - // Handle edge cases where we end up with just HTTP method or too short - if (!simplified || simplified.length < 2) { - return operationId.charAt(0).toLowerCase() + operationId.slice(1); - } + const strategy = input.strategy ?? 'flat'; - return simplified; + return context.valueToObject({ + defaultValue: { + nesting: 'operationId', + nestingDelimiters: /[./]/, + strategy, + strategyDefaultTag: 'default', + }, + mappers: { + object(value) { + value.keyName = context.valueToObject({ + defaultValue: { casing: 'camelCase' }, + mappers: { + function: (name) => ({ name }), + string: (name) => ({ name }), + }, + value: value.keyName, + }); + value.segmentName = context.valueToObject({ + defaultValue: { casing: 'camelCase' }, + mappers: { + function: (name) => ({ name }), + string: (name) => ({ name }), + }, + value: value.segmentName, + }); + return value; + }, + }, + value: input, + }) as RouterConfig; } export const defaultConfig: OrpcContractPlugin['Config'] = { config: { contractNameBuilder: (id: string) => `${id}Contract`, - defaultTag: 'default', exportFromIndex: false, - groupKeyBuilder: defaultGroupKeyBuilder, - operationKeyBuilder: defaultOperationKeyBuilder, + router: { + keyName: { casing: 'camelCase' }, + nesting: 'operationId', + nestingDelimiters: /[./]/, + segmentName: { casing: 'camelCase' }, + strategy: 'flat', + strategyDefaultTag: 'default', + }, routerName: { name: 'router' }, validator: 'zod', }, handler, name: '@orpc/contract', - resolveConfig: (plugin) => { + resolveConfig: (plugin, context) => { plugin.config.exportFromIndex ??= false; plugin.config.contractNameBuilder ??= (id: string) => `${id}Contract`; - plugin.config.defaultTag ??= 'default'; - plugin.config.groupKeyBuilder ??= defaultGroupKeyBuilder; - plugin.config.operationKeyBuilder ??= defaultOperationKeyBuilder; + plugin.config.router = resolveRouter(plugin.config.router, context); plugin.config.routerName = resolveNaming(plugin.config.routerName); if (!plugin.config.routerName.name) { plugin.config.routerName.name = 'router'; diff --git a/packages/openapi-ts/src/plugins/@orpc/contract/plugin.ts b/packages/openapi-ts/src/plugins/@orpc/contract/plugin.ts index 4e563d1e1..26d640c9b 100644 --- a/packages/openapi-ts/src/plugins/@orpc/contract/plugin.ts +++ b/packages/openapi-ts/src/plugins/@orpc/contract/plugin.ts @@ -1,9 +1,13 @@ +import type { NodeName } from '@hey-api/codegen-core'; + import type { IR } from '~/ir/types'; +import { OperationPath, OperationStrategy } from '~/openApi/shared/locations'; import { createOperationComment } from '~/plugins/shared/utils/operation'; import { $ } from '~/ts-dsl'; +import type { ObjectTsDsl } from '~/ts-dsl/expr/object'; import { applyNaming, toCase } from '~/utils/naming'; -import type { OrpcContractPlugin } from './types'; +import type { OrpcContractPlugin, RouterConfig } from './types'; function hasInput(operation: IR.OperationObject): boolean { const hasPathParams = Boolean( @@ -49,15 +53,61 @@ function getTags(operation: IR.OperationObject, defaultTag: string): string[] { : [defaultTag]; } +function getOperationPaths( + operation: IR.OperationObject, + routerConfig: RouterConfig, +): ReadonlyArray> { + const { nesting, nestingDelimiters, strategy, strategyDefaultTag } = + routerConfig; + + // Get path derivation function + let pathFn = OperationPath.id(); + if (typeof nesting === 'function') { + pathFn = nesting; + } else if (nesting === 'operationId') { + pathFn = OperationPath.fromOperationId({ delimiters: nestingDelimiters }); + } + + // Get structure strategy function + let strategyFn; + if (typeof strategy === 'function') { + strategyFn = strategy; + } else if (strategy === 'byTags') { + strategyFn = OperationStrategy.byTags({ + fallback: strategyDefaultTag, + path: pathFn, + }); + } else if (strategy === 'single') { + strategyFn = OperationStrategy.single({ + path: pathFn, + root: strategyDefaultTag, + }); + } else { + // flat + strategyFn = OperationStrategy.flat({ path: pathFn }); + } + + return strategyFn(operation); +} + +type NestedLeaf = { type: 'leaf'; value: NodeName }; +type NestedNode = { children: Map; type: 'node' }; +type NestedValue = NestedLeaf | NestedNode; + +function buildNestedObject(node: NestedNode): ObjectTsDsl { + const obj = $.object(); + for (const [key, child] of node.children) { + if (child.type === 'leaf') { + obj.prop(key, $(child.value)); + } else { + obj.prop(key, buildNestedObject(child)); + } + } + return obj; +} + export const handler: OrpcContractPlugin['Handler'] = ({ plugin }) => { - const { - contractNameBuilder, - defaultTag, - groupKeyBuilder, - operationKeyBuilder, - routerName, - validator, - } = plugin.config; + const { contractNameBuilder, router, routerName, validator } = plugin.config; const operations: IR.OperationObject[] = []; @@ -96,7 +146,7 @@ export const handler: OrpcContractPlugin['Handler'] = ({ plugin }) => { for (const op of operations) { const contractName = contractNameBuilder(op.id); - const tags = getTags(op, defaultTag); + const tags = getTags(op, router.strategyDefaultTag); const successResponse = getSuccessResponse(op); const contractSymbol = plugin.symbol(contractName, { @@ -180,30 +230,44 @@ export const handler: OrpcContractPlugin['Handler'] = ({ plugin }) => { }, }); - const operationsByGroup = new Map(); - for (const op of operations) { - const groupKey = groupKeyBuilder(op); - if (!operationsByGroup.has(groupKey)) { - operationsByGroup.set(groupKey, []); - } - operationsByGroup.get(groupKey)!.push(op); - } + // Build nested structure using a tree + const root: NestedNode = { children: new Map(), type: 'node' }; - const contractsObject = $.object().pretty(); - for (const [groupKey, groupOps] of operationsByGroup) { - const groupObject = $.object(); + for (const op of operations) { + const contractSymbol = contractSymbols[op.id]; + if (contractSymbol) { + const paths = getOperationPaths(op, router); + for (const path of paths) { + let current: NestedNode = root; + for (let i = 0; i < path.length; i++) { + const isLast = i === path.length - 1; + const segment = isLast + ? applyNaming(path[i]!, router.keyName) + : applyNaming(path[i]!, router.segmentName); - for (const op of groupOps) { - const contractSymbol = contractSymbols[op.id]; - if (contractSymbol) { - const key = operationKeyBuilder(op.id, groupKey); - groupObject.prop(key, $(contractSymbol)); + if (isLast) { + current.children.set(segment, { + type: 'leaf', + value: contractSymbol, + }); + } else { + if (!current.children.has(segment)) { + current.children.set(segment, { + children: new Map(), + type: 'node', + }); + } + const next = current.children.get(segment)!; + if (next.type === 'node') { + current = next; + } + } + } } } - - contractsObject.prop(groupKey, groupObject); } + const contractsObject = buildNestedObject(root).pretty(); const contractsNode = $.const(contractsSymbol) .export() .assign(contractsObject); diff --git a/packages/openapi-ts/src/plugins/@orpc/contract/types.d.ts b/packages/openapi-ts/src/plugins/@orpc/contract/types.d.ts index 79781d7a2..7f78d6c6e 100644 --- a/packages/openapi-ts/src/plugins/@orpc/contract/types.d.ts +++ b/packages/openapi-ts/src/plugins/@orpc/contract/types.d.ts @@ -1,8 +1,107 @@ -import type { IR } from '~/ir/types'; +import type { + OperationPathStrategy, + OperationsStrategy, +} from '~/openApi/shared/locations'; import type { DefinePlugin, Plugin } from '~/plugins'; import type { PluginValidatorNames } from '~/plugins/types'; import type { NamingConfig, NamingRule } from '~/utils/naming'; +export interface UserRouterConfig { + /** + * Customize key names for operations in the router. + * + * Applied to the final segment of the path (the key name). + */ + keyName?: NamingRule; + /** + * How to derive nesting structure from operations. + * + * - `'operationId'` - Split operationId by delimiters (e.g., `users.list` → `{ users: { list } }`) + * - `'id'` - Use operation id as-is, no nesting + * - Custom function for full control + * + * @default 'operationId' + */ + nesting?: 'id' | 'operationId' | OperationPathStrategy; + /** + * Delimiters for splitting operationId. + * + * Only applies when `nesting` is `'operationId'`. + * + * @default /[./]/ + */ + nestingDelimiters?: RegExp; + /** + * Customize segment names for nested groups. + * + * Applied to intermediate path segments (not the key name). + */ + segmentName?: NamingRule; + /** + * Grouping strategy. + * + * - `'flat'` - No grouping, all contracts at root level + * - `'byTags'` - One group per operation tag + * - `'single'` - All operations in one group + * - Custom function for full control + * + * @default 'flat' + */ + strategy?: OperationsStrategy; + /** + * Default group name for operations without tags. + * + * Only applies when `strategy` is `'byTags'`. + * + * @default 'default' + */ + strategyDefaultTag?: string; +} + +export interface RouterConfig { + /** + * Customize key names for operations in the router. + * + * Applied to the final segment of the path (the key name). + */ + keyName: NamingConfig; + /** + * How to derive nesting structure from operations. + * + * - `'operationId'` - Split operationId by delimiters (e.g., `users.list` → `{ users: { list } }`) + * - `'id'` - Use operation id as-is, no nesting + * - Custom function for full control + */ + nesting: 'id' | 'operationId' | OperationPathStrategy; + /** + * Delimiters for splitting operationId. + * + * Only applies when `nesting` is `'operationId'`. + */ + nestingDelimiters: RegExp; + /** + * Customize segment names for nested groups. + * + * Applied to intermediate path segments (not the key name). + */ + segmentName: NamingConfig; + /** + * Grouping strategy. + * + * - `'flat'` - No grouping, all contracts at root level + * - `'byTags'` - One group per operation tag + * - `'single'` - All operations in one group + * - Custom function for full control + */ + strategy: OperationsStrategy; + /** + * Default group name for operations without tags. + * + * Only applies when `strategy` is `'byTags'`. + */ + strategyDefaultTag: string; +} + export type UserConfig = Plugin.Name<'@orpc/contract'> & Plugin.Hooks & { /** @@ -11,12 +110,6 @@ export type UserConfig = Plugin.Name<'@orpc/contract'> & * @default (id) => `${id}Contract` */ contractNameBuilder?: (operationId: string) => string; - /** - * Default tag name for operations without tags. - * - * @default 'default' - */ - defaultTag?: string; /** * Whether exports should be re-exported in the index file. * @@ -24,18 +117,26 @@ export type UserConfig = Plugin.Name<'@orpc/contract'> & */ exportFromIndex?: boolean; /** - * Custom function to extract group key for router grouping. - * Receives the full IR.OperationObject. + * Router configuration for grouping and nesting operations. * - * @default extracts first path segment as camelCase - */ - groupKeyBuilder?: (operation: IR.OperationObject) => string; - /** - * Custom function to generate operation key within a group. + * Can be a strategy string for simple cases, or an object for full control. + * + * @default { strategy: 'flat' } * - * @default (operationId, groupKey) => simplified operationId + * @example + * // Simple: just set strategy + * router: 'byTags' + * + * @example + * // Full control + * router: { + * strategy: 'byTags', + * nesting: 'operationId', + * segmentName: { casing: 'camelCase' }, + * keyName: { casing: 'camelCase' }, + * } */ - operationKeyBuilder?: (operationId: string, groupKey: string) => string; + router?: OperationsStrategy | UserRouterConfig; /** * Naming rule for the router export. * The type export will be the PascalCase version (e.g., 'router' → 'Router'). @@ -69,11 +170,9 @@ export type UserConfig = Plugin.Name<'@orpc/contract'> & export type Config = Plugin.Name<'@orpc/contract'> & Plugin.Hooks & { contractNameBuilder: (operationId: string) => string; - defaultTag: string; exportFromIndex: boolean; - groupKeyBuilder: (operation: IR.OperationObject) => string; - operationKeyBuilder: (operationId: string, groupKey: string) => string; output: string; + router: RouterConfig; routerName: NamingConfig; validator: PluginValidatorNames; }; -- 2.54.0