Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b09d2ea
fix(common): refine ts/zod and ts/static exports
Wesley-Young Apr 13, 2026
46dc3e9
Merge branch 'main' into dev
Wesley-Young May 1, 2026
9b8547f
Merge branch 'main' into dev
Wesley-Young May 1, 2026
22bd792
feat(protocol): add group_disband event
Wesley-Young May 2, 2026
bf90b77
feat(protocol): make time configurable for OutgoingForwardMessage
Wesley-Young May 2, 2026
8bd1d62
update: bump version to 1.3.0-rc.1
Wesley-Young May 2, 2026
22e681c
Merge branch 'main' into dev
Wesley-Young May 2, 2026
60a92d7
chore: fix zod generator format
Wesley-Young May 2, 2026
9cba122
feat(protocol): add persist_group_file API
Wesley-Young May 9, 2026
fe0d489
feat(protocol): add Markdown segment for incoming
Wesley-Young May 9, 2026
cb14e12
refactor: remove official types package
Wesley-Young May 9, 2026
7c7ace6
Merge branch 'main' into dev
Wesley-Young May 9, 2026
4cb16d7
fix(protocol): append `since` to OutgoingFaceSegment.is_large
Wesley-Young May 9, 2026
39e41bd
Merge branch 'main' into dev
Wesley-Young May 9, 2026
f6a6f5e
feat: expose plain api endpoint tree in typescript codegen
Wesley-Young May 11, 2026
1c4aef0
Merge branch 'main' into dev
Wesley-Young May 29, 2026
cd8eae1
fix(common): align with zod nullish behavior
Wesley-Young May 29, 2026
07f7b55
feat(common): add generation for z.input projection
Wesley-Young May 30, 2026
5494384
fix(types): correct protocol version
Wesley-Young Jun 4, 2026
12f30b9
fix(generator): correct README usage
Wesley-Young Jun 4, 2026
1e41526
Merge branch 'main' into dev
Wesley-Young Jun 16, 2026
51a8ef5
Merge branch 'main' into dev
Wesley-Young Jun 24, 2026
f917b13
feat(types): add field `is_self_send` (def: false) to `get_private_fi…
Wesley-Young Jun 24, 2026
3178227
update(types): bump version to 1.3.0-rc.2
Wesley-Young Jun 24, 2026
1eee123
fix(common): correct zodAppEndpoints indentation
Wesley-Young Jul 10, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
*.g.ts

### IntelliJ IDEA ###
.idea/
Expand Down
31 changes: 28 additions & 3 deletions common/src/generator/typescript/shared.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import type { IRField } from '@saltify/milky-protocol';

export function getTypeScriptTypeProjection(field: IRField): string {
export type TypeScriptTypeProjectionMode = 'input' | 'output';

export interface TypeScriptTypeProjectionOptions {
mode?: TypeScriptTypeProjectionMode;
getRefTypeName?: (name: string, mode: TypeScriptTypeProjectionMode) => string;
}

function getDefaultRefTypeName(name: string, mode: TypeScriptTypeProjectionMode): string {
if (mode === 'input') {
return `${name}_ZodInput`;
}
return name;
}

export function getTypeScriptTypeProjection(field: IRField, options: TypeScriptTypeProjectionOptions = {}): string {
const mode = options.mode ?? 'output';
const getRefTypeName = options.getRefTypeName ?? getDefaultRefTypeName;

if (field.isOptional) {
return `${getTypeScriptTypeProjection({ ...field, isOptional: false }, options)} | null | undefined`;
}

if (mode === 'input' && field.defaultValue !== undefined) {
return `${getTypeScriptTypeProjection({ ...field, defaultValue: undefined }, options)} | null | undefined`;
}

if (field.fieldType === 'scalar') {
if (field.scalarType === 'string') {
if (field.isArray) {
Expand Down Expand Up @@ -35,9 +60,9 @@ export function getTypeScriptTypeProjection(field: IRField): string {

if (field.fieldType === 'ref') {
if (field.isArray) {
return `${field.refStructName}[]`;
return `${getRefTypeName(field.refStructName, mode)}[]`;
} else {
return field.refStructName;
return getRefTypeName(field.refStructName, mode);
}
}

Expand Down
198 changes: 160 additions & 38 deletions common/src/generator/typescript/static.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
import type { IR } from '@saltify/milky-protocol';
import type { IR, IRField } from '@saltify/milky-protocol';

import { getApiTypeNames } from '../shared/ir';
import { normalizeDerivedStructName } from '../shared/naming';
import { createLineWriter, formatBlockDocComment } from '../shared/text';
import { getTypeScriptTypeProjection } from './shared';
import { getTypeScriptTypeProjection, type TypeScriptTypeProjectionMode } from './shared';

function getZodInputTypeName(name: string): string {
return `${name}_ZodInput`;
}

function getTypeScriptFieldProjection(field: IRField, mode: TypeScriptTypeProjectionMode): string {
return getTypeScriptTypeProjection(field, { mode });
}

function isFieldOptionalInProjection(field: IRField, mode: TypeScriptTypeProjectionMode): boolean {
return field.isOptional || (mode === 'input' && field.defaultValue !== undefined);
}

function renderField(
l: (line?: string) => void,
field: IRField,
mode: TypeScriptTypeProjectionMode,
indent: string = ' ',
) {
formatBlockDocComment(field.description, field.since, indent).forEach((line) => {
l(line);
});
l(
`${indent}${field.name}${isFieldOptionalInProjection(field, mode) ? '?' : ''}: ${getTypeScriptFieldProjection(field, mode)};`,
);
}

export function generateTypeScriptStaticSpec(ir: IR): string {
const writer = createLineWriter();
Expand All @@ -25,10 +51,16 @@ export function generateTypeScriptStaticSpec(ir: IR): string {
});
l(`export interface ${struct.name} {`);
struct.fields.forEach((field) => {
formatBlockDocComment(field.description, field.since, ' ').forEach((line) => {
l(line);
});
l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`);
renderField(l, field, 'output');
});
l('}');
l();
formatBlockDocComment(struct.description, struct.since).forEach((line) => {
l(line);
});
l(`export interface ${getZodInputTypeName(struct.name)} {`);
struct.fields.forEach((field) => {
renderField(l, field, 'input');
});
l('}');
} else {
Expand All @@ -42,10 +74,20 @@ export function generateTypeScriptStaticSpec(ir: IR): string {
l(` /** 数据类型区分字段,表示自身为${derivedStruct.description} */`);
l(` ${struct.tagFieldName}: '${derivedStruct.tagValue}';`);
derivedStruct.fields.forEach((field) => {
formatBlockDocComment(field.description, field.since, ' ').forEach((line) => {
l(line);
});
l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`);
renderField(l, field, 'output');
});
l('}');
l();
formatBlockDocComment(derivedStruct.description, derivedStruct.since).forEach((line) => {
l(line);
});
l(
`export interface ${getZodInputTypeName(normalizeDerivedStructName(struct.name, derivedStruct.tagValue))} {`,
);
l(` /** 数据类型区分字段,表示自身为${derivedStruct.description} */`);
l(` ${struct.tagFieldName}: '${derivedStruct.tagValue}';`);
derivedStruct.fields.forEach((field) => {
renderField(l, field, 'input');
});
l('}');
l();
Expand All @@ -59,47 +101,87 @@ export function generateTypeScriptStaticSpec(ir: IR): string {
` | ${normalizeDerivedStructName(struct.name, derivedStruct.tagValue)}${index === struct.derivedStructs.length - 1 ? ';' : ''}`,
);
});
l();
formatBlockDocComment(struct.description, struct.since).forEach((line) => {
l(line);
});
l(`export type ${getZodInputTypeName(struct.name)} =`);
struct.derivedStructs.forEach((derivedStruct, index) => {
l(
` | ${getZodInputTypeName(normalizeDerivedStructName(struct.name, derivedStruct.tagValue))}${index === struct.derivedStructs.length - 1 ? ';' : ''}`,
);
});
} else {
// with data property
struct.derivedTypes.forEach((derivedType) => {
const derivedStructName = normalizeDerivedStructName(struct.name, derivedType.tagValue);
formatBlockDocComment(derivedType.description, derivedType.since).forEach((line) => {
l(line);
});
l(`export interface ${derivedStructName} {`);
l(` /** 数据类型区分字段,表示自身为${derivedType.description} */`);
l(` ${struct.tagFieldName}: '${derivedType.tagValue}';`);
struct.baseFields.forEach((field) => {
renderField(l, field, 'output');
});

if (derivedType.derivingType === 'struct') {
l(`export interface ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data {`);
l(` /** 数据内容 */`);
l(` data: {`);
derivedType.fields.forEach((field) => {
formatBlockDocComment(field.description, field.since, ' ').forEach((line) => {
l(line);
});
l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`);
renderField(l, field, 'output', ' ');
});
l('}');
l(' }');
} else {
// ref type
l(
`export type ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data = ${derivedType.refStructName};`,
);
l(` /** 数据内容 */`);
l(` data: ${derivedType.refStructName};`);
}
// add type aliases for Event
if (struct.name === 'Event') {
formatBlockDocComment(derivedType.description, derivedType.since).forEach((line) => {
l(line);
l('}');
l();
formatBlockDocComment(derivedType.description, derivedType.since).forEach((line) => {
l(line);
});
l(`export interface ${getZodInputTypeName(derivedStructName)} {`);
l(` /** 数据类型区分字段,表示自身为${derivedType.description} */`);
l(` ${struct.tagFieldName}: '${derivedType.tagValue}';`);
struct.baseFields.forEach((field) => {
renderField(l, field, 'input');
});

if (derivedType.derivingType === 'struct') {
l(` /** 数据内容 */`);
l(` data: {`);
derivedType.fields.forEach((field) => {
renderField(l, field, 'input', ' ');
});
l(
`export type ${normalizeDerivedStructName(struct.name, derivedType.tagValue)} = ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data;`,
);
l(' }');
} else {
// ref type
l(` /** 数据内容 */`);
l(` data: ${getZodInputTypeName(derivedType.refStructName)};`);
}
l('}');
l();
});
formatBlockDocComment(struct.description, struct.since).forEach((line) => {
l(line);
});
l(`export type ${struct.name} =`);
struct.derivedTypes.forEach((derivedType, index) => {
l(' | {');
l(` ${struct.tagFieldName}: '${derivedType.tagValue}';`);
l(` data: ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}Data;`);
l(` }${index === struct.derivedTypes.length - 1 ? ';' : ''}`);
l(
` | ${normalizeDerivedStructName(struct.name, derivedType.tagValue)}${index === struct.derivedTypes.length - 1 ? ';' : ''}`,
);
});
l();
formatBlockDocComment(struct.description, struct.since).forEach((line) => {
l(line);
});
l(`export type ${getZodInputTypeName(struct.name)} =`);
struct.derivedTypes.forEach((derivedType, index) => {
l(
` | ${getZodInputTypeName(normalizeDerivedStructName(struct.name, derivedType.tagValue))}${index === struct.derivedTypes.length - 1 ? ';' : ''}`,
);
});
}
}
Expand All @@ -119,32 +201,52 @@ export function generateTypeScriptStaticSpec(ir: IR): string {
if (api.requestFields !== undefined) {
l(`export interface ${typeNames.inputName} {`);
api.requestFields.forEach((field) => {
formatBlockDocComment(field.description, field.since, ' ').forEach((line) => {
l(line);
});
l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`);
renderField(l, field, 'output');
});
l('}');
} else {
l(`export type ${typeNames.inputName} = {};`);
}
l();
formatBlockDocComment(`${api.description} API 请求参数`, api.since).forEach((line) => {
l(line);
});
if (api.requestFields !== undefined) {
l(`export interface ${getZodInputTypeName(typeNames.inputName)} {`);
api.requestFields.forEach((field) => {
renderField(l, field, 'input');
});
l('}');
} else {
l(`export type ${getZodInputTypeName(typeNames.inputName)} = {};`);
}
l();
formatBlockDocComment(`${api.description} API 响应数据`, api.since).forEach((line) => {
l(line);
});
if (api.responseFields !== undefined) {
l(`export interface ${typeNames.outputName} {`);
api.responseFields.forEach((field) => {
formatBlockDocComment(field.description, field.since, ' ').forEach((line) => {
l(line);
});
l(` ${field.name}${field.isOptional ? '?' : ''}: ${getTypeScriptTypeProjection(field)};`);
renderField(l, field, 'output');
});
l('}');
} else {
l(`export type ${typeNames.outputName} = {};`);
}
l();
formatBlockDocComment(`${api.description} API 响应数据`, api.since).forEach((line) => {
l(line);
});
if (api.responseFields !== undefined) {
l(`export interface ${getZodInputTypeName(typeNames.outputName)} {`);
api.responseFields.forEach((field) => {
renderField(l, field, 'input');
});
l('}');
} else {
l(`export type ${getZodInputTypeName(typeNames.outputName)} = {};`);
}
l();
});
});
l('export interface ApiCategories {');
Expand All @@ -158,13 +260,33 @@ export function generateTypeScriptStaticSpec(ir: IR): string {
});
l(` ${api.endpoint}: {`);
l(` request: ${typeNames.inputName};`);
l(` request_ZodInput: ${getZodInputTypeName(typeNames.inputName)};`);
l(` response: ${typeNames.outputName};`);
l(` response_ZodInput: ${getZodInputTypeName(typeNames.outputName)};`);
l(' };');
});
l(' };');
});
l('}');
l();
l('export interface ApiEndpoints {');
ir.apiCategories.forEach((category) => {
category.apis.forEach((api) => {
const typeNames = getApiTypeNames(api.endpoint);
formatBlockDocComment(api.description, api.since, ' ').forEach((line) => {
l(line);
});
l(` /** ${api.description} */`);
l(` '${api.endpoint}': {`);
l(` request: ${typeNames.inputName};`);
l(` request_ZodInput: ${getZodInputTypeName(typeNames.inputName)};`);
l(` response: ${typeNames.outputName};`);
l(` response_ZodInput: ${getZodInputTypeName(typeNames.outputName)};`);
l(' };');
});
});
l('}');
l();

return writer.toString();
}
Loading