egoist

Stars and mentions

Last 30 days, Oct 5 – Nov 3

GitHub stars725
mentions71
017345168Oct 5Oct 19Nov 3
UTC days · repositories egoist owns44 repositories

Package downloads

Cumulative recorded downloads · since 2025-10-19

crates.io—
npm345.8M
086.5M172.9M259.4M345.8MOct 5Oct 19Nov 3
Oct 24crates.io: 0/5 packages observed · npm: 2/28 packages observed

Across 33 known packages in repositories egoist owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

egoist’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside egoist’s account where egoist opened or reviewed pull requests in the past 365 days.

Who noticed egoist

Everyone who starred or forked egoist’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

67posts
Nov 40 posts
  1. V2EX · · 程序员 · @bronyakaka · egoist/mygo

    我把自己的 Agent 从 Wails 迁到了 MyGo 原生 GUI,全程踩坑和实践分享 最近把自己写的工作 agent [Ally agent]( Bronya0/ally-agent) 的界面重写了一遍,起因是无意中看到有人推荐 MyGo ,加上我对内存和性能一直有点追求,多年来一直在用 wails ,想换个试试,就想拿自己的项目试点,没想到结果意外的不错。 旧版本的技术栈是典型的 Go + Wails + Vue (wails 我用了很多年 最近终于 V3 发正式版了 等了四年,可惜我也老了),界面用 Naive UI ,样式美观,效果也相当不错。但做久了有几个地方比较麻烦( tarui 应该也是一样): 跨平台界面的细节不一致,构建链比较长,还有前端内存出了问题以后,我这个写后端的人不太容易排查。 折腾了一段时间,现在主要页面已经迁到 [MyGo]( egoist/mygo) 了,记录一下过程中比较有感触的地方。 先说一下技术路线:Wails 的界面通过系统 WebView 展示网页; MyGo 既支持这种方式,也支持用 Go 写 Native UI ,由框架自己绘制。WebView 也有自己的优势,MyGo 两种都支持,相当于有个兜底,下面会说。 Ally 现在的聊天、设置等常用页面用 Native UI ,插件页面和部分预览仍然按需使用 WebView 。 一、UI 跨平台一致性 这是我这次迁移最重视的一点。 之前用 Wails 做界面,Windows 上调好的页面,到了 macOS 上经常还要再看一遍。字体、行高、滚动条、输入框这些地方,偶尔就会有一点不一样。 比如我画的表格,不知道为啥线条在 macOS 上看着发虚,Windows 上就正常。单看问题都不大,但调整完一个平台还得顾着另一个平台,挺烦的。 Wails 使用系统 WebView ,Windows 和 macOS 背后的浏览器引擎不一样,前端样式最终要经过各自的渲染实现。 MyGo 的 Native UI 由框架自己负责布局和绘制。迁移后,我在 Windows 上调好的主体界面,拿到 macOS 上看到的效果接近得多,需要针对平台单独修的地方也少了。 当然,自绘也不等于所有像素都完全一致。MyGo 的文字排版仍然使用[各平台的文字系统]( mygo.egoist.dev):Windows 是 DirectWrite ,macOS 是 Core Text ,Linux 是 Pango 。字体回退、DPI 、输入法这些还是要分别检查。但至少不用再同时处理两套 WebView 引擎带来的样式差异了。 以前总觉得 Electron 重,碰到这些问题后倒是能理解为什么有些项目愿意用它:自带 Chromium ,渲染环境相对好统一。OpenCode 桌面端后来也[从 Tauri 换到了 Electron]( github.com/anomalyco/opencode/blob/dev/packages/d…),不过人家具体为什么换,我就不瞎猜了。 二、内存问题现在能用熟悉的 Go 工具查 以前 Wails 版本的 Go 后端一样可以用 pprof,但界面在 WebView 里,Go 堆、JavaScript 堆、DOM 、浏览器渲染进程是几块不同的东西。 前端也有 DevTools ,可以拍 JS heap snapshot 、查 DOM 引用。但我平时主要写 Go ,遇到界面内存持续上涨,分析起来没有 Go 那么顺手。 现在主要界面和业务都在 Go 进程里,我给 Ally 加了一个按需导出诊断的入口:在设置里点“导出内存诊断”,会导出 heap.pprof、allocs.pprof、goroutine.pprof 等文件。 比如在空闲时导出一次,复现内存增长后再导出一次,就可以对比: go tool pprof -http=127.0.0.1:0 \ -base=before/heap.pprof after/heap.pprof heap 主要看还活着的对象,allocs 主要看累计分配。分配量大不一定是泄漏,也可能是某段代码反复创建临时对象,GC 能回收,但会持续消耗 CPU 。 当然,pprof 主要看到的是 Go 堆。系统和 GPU 资源,以及 WebView 子进程的内存,还是得用其他工具看。 三、帧处理的坑 MyGo 的界面更新会重新构建控件树,写 UI 时要特别留意渲染函数里做了什么。 我的 Agent 有命令执行卡片,命令输出可能很长。之前即使卡片折叠着、界面上只展示最后几行,代码仍然会先把整段输出拆分、去空行,再取出末尾几行。 看起来只是一个字符串处理函数,但界面更新时会反复执行。 后来改了两处: 1. 只需要末尾几行时,从字符串尾部向前扫描,不再先拆分整个输出。 2. 需要重复使用的派生结果按原始内容缓存;内容没变就复用。折叠时不构造展开用的行数据,并释放对应缓存。 一次本地测试里,用两万行命令输出测试折叠卡片的相关处理,单次分配从约 1.9 MB 降到 1.9 KB ,耗时从约 0.72 ms 降到 0.02 ms。 这是这段处理在该样本下的局部结果,不是整个应用的性能对比。[对应修改在这里]( github.com/Bronya0/ally-agent/commit/a1e085b9397e…)。 类似的问题还有工具参数 JSON:同一条消息的卡片头、卡片体都可能读取参数,如果每次渲染都 Unmarshal 一遍,长对话里分配会越来越明显。现在把解析结果挂在控件局部状态上,原始参数变化时才重新解析。 我的感受是,性能问题要先分清楚是“每次请求执行一次”,还是“界面更新时每帧执行一次”。后者哪怕单次看着不重,乘上帧数和控件数量也很拖累性能。 四、构建链变短 旧项目的构建大致要经过前端依赖、绑定生成、Vite 构建,再编译 Go 。MyGo Native UI 的主要页面直接用 Go 写,主界面去掉了前端打包这一步,我自己的项目构建起来明显快了不少。 MyGo 不依赖 CGO ,交叉编译也省事。目前 Ally 的构建脚本可以在 macOS 上处理 Windows 、macOS 、Linux 的 x64 和 ARM64 六个目标,另外生成 macOS Universal 包。 例如编译一个 Windows x64 二进制,核心就是: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o Ally.exe . 实际发布还要处理资源、压缩包、签名等事情;能交叉编译出来,也不代表已经在目标系统完成运行验证。Linux 的图形环境和使用 WebView 时需要的系统库,也仍然要考虑。 这里补充一下:我刚看了 [Wails 3.1 的发布说明]( wails.io),它也加入了无 CGO 构建能力。目前 macOS 和 Linux 的 purego 后端还是实验性质,CGO 仍是默认选项。 所以“无 CGO”以后不一定是两者之间的主要区别。对我这个项目来说,更明显的差别是主界面不用再经过 Vue 那套构建流程了。 五、MyGo 组件够用,但 Web 生态还是强很多 迁移前我担心原生 GUI 要从头写一堆控件,实际比想象中好一些。 MyGo 已经有常见的表单、列表、树、弹窗,也有代码编辑器、Diff 、Markdown 等组件。Ally 的文件浏览和代码编辑用到了它的编辑器,聊天内容也用到了 Markdown 相关能力。 但如果和成熟的 Web 组件库比,差距还是很明显。Naive UI 这类库积累了很长时间,一个表单、选择器或者弹窗拿来就能用,样式、焦点、键盘操作、动画都比较完整。 好在 MyGo 的定制能力不错,但要做出自己满意的效果,颜色、间距、状态和交互细节还是得花时间。我之前不太想碰这类原生 UI ,一个原因就是觉得手写样式太累。 现在有 AI 帮忙,这件事容易太多了。先用框架已有组件和 Base/Parts API ,让 AI 根据目标效果调整样式,再自己检查实际交互。 我的经验是,尽量复用组件本身的焦点、键盘导航、浮层和滚动行为,别为了改外观把这些重新实现一遍,非常容易踩坑。 六、WebView 的优势还在,可以混合使用 WebView 最大的好处之一就是生态。 视频播放、复杂图表、各种文件预览,Web 上已经有大量现成方案。很多东西如果坚持全部原生重写,维护成本很高。 MyGo 可以在 Native UI 窗口里嵌入 WebView ,所以我的做法是:常驻界面用原生 UI ,需要 Web 生态的地方再创建 WebView 。 Ally 目前的 HTML 和 Mermaid 预览就是这样处理的。预览可见时按需创建,数量做限制,离开后释放。否则一个长对话里出现很多图表时,每个预览都长期挂着 WebView ,前面省下来的资源很容易又花回去。 迁移后日常使用的内存占用和响应都有比较明显的改善。不过我还没有整理同一机器、相同会话和操作条件下的完整对照数据,暂时不拿具体数字比较两个框架。 七、插件化是 Go 里比较麻烦的东西,需要谨慎考虑 这是迁移前有点低估、做起来以后感受很深的一点。 看看 [DeepSeek Harness 的 Web Client 架构]( github.com/deepseek-ai/deepseek-harness/blob/mast…),它有插件加载、服务、事件和 UI Slots ,插件可以在已有界面里贡献功能。 Web 技术和 JavaScript 的动态加载能力,让这类扩展比较容易分发和迭代。当然,WebView 本身不会自动提供插件体系,生命周期、扩展点和权限边界都得应用自己设计。 纯 Go 桌面应用要做同样的事就麻烦得多。[Go 标准库的 plugin]( pkg.go.dev) 不支持 Windows ,加载后也不能卸载,对编译工具链和依赖版本还有要求,不适合直接拿来做一个跨 Windows 、macOS 、Linux 的热插拔 GUI 插件市场。 最后 Ally 还是用了混合方案:主程序是原生 UI ,插件页面按需跑在 WebView 里。插件可以打成 zip 安装,在侧栏增加页面,通过宿主提供的接口访问存储、HTTP 和工作区等能力;重新导入后可以更新,不用重启整个应用。 目前这更接近“可插拔页面”,和 DSH 那种能扩展更多核心能力的插件体系不太一样。WebView 也有自己的生命周期和资源成本,要做成完整生态,扩展点、兼容性和权限模型还有不少事要解决。 代码 大概就这些,迁移完之后我自己还是比较满意的,欢迎大家一起尝试原生界面 我的项目代码在 [Ally 仓库]( Bronya0/ally-agent): Bronya0/ally-agent 原来的 Wails 版本还在 main 分支,MyGo 版本在 [mygo 分支]( github.com/Bronya0/ally-agent/tree/mygo),感兴趣或想学习 MyGo 的可以对比着看,欢迎点个 Star 。

    15 comments
  2. 用 MyGo 实现了一个无限画布,纯属一时兴起,半小时就实现了 egoist/godraw t.co

    90 likes · 2 reposts · 5,814 views
  3. I (or Claude actually) made an infinite canvas in MyGo under 1 hour, for absolutely no reason at all, it's like tldraw but in pure Go egoist/godraw t.co

    369 likes · 18 reposts · 11,098 views
  4. I made a music player in MyGo, you probably can't use it cause it only supports Chinese streaming services currently, but feel free to fork it and make your own egoist/mygo-cloudmusic t.co

    59 likes · 1 repost · 3,605 views
  5. Zenn · · @inflab · egoist/tsup

    Code generator開発記(Part 2) ! この記事は2023年7月3日に執筆されたものです。 こんにちは。Inflabのフロントエンドエンジニア、Lucasです。 前回の記事では、code generatorの必要性と、自前で実装するに至るまでの悩みを共有しました。今回は、code generatorをはじめ、schema validator、type guardをどのように実装したのかを簡単にご紹介したいと思います。 最初にお伝えしておくと、これはとてもシンプルなアイデアで、実装に複雑な技術は必要ありません。その前提で読み進めていただければと思います。 1. 目標と必要な機能 まず、前回の記事で立てた目標について改めて考えてみます。私はcode generatorを実装する前に、次のような目標を立てました。 ファイルまたはネットワークリクエストでOpenAPI specification JSONを直接パースし、望む形の結果をファイルとして生成できること。 フロントエンドエンジニアが、APIのschemaの変化にできるだけ手間なく対応できること。 フロントエンドエンジニアが、生成物として出力したいAPIを選択できること。 APIで宣言したresponseをJSON validatorで検証できるtype guardが自動生成されること。 バックエンドエンジニアが、フロントエンドの手間を気にせずAPIを作れること。 バックエンドエンジニアが宣言したSwaggerと実際のAPI responseの形が一致しているかを、integration testで検証できること。 特に最後の目標は、Elixirのdoc testの概念を借りたものです。昨年取り組んだ個人プロジェクトでテストを書いた際の感触が良く、GraphQLがサポートしているようなschemaの整合性検証も、近い形なら実現できるのではないかと期待していました。特に私たちのバックエンドチームはテストを書くことを非常に重視しているので、OpenAPI specificationと実際のAPIの結果をテストする手軽な方法を提供できるだろうと考えました。 以下、OpenAPI specification JSONはOASと略します。 2. 使用したライブラリ まず、実装に使ったライブラリを紹介します。おそらくほとんどの方がご存じだと思います。 1. 実装 inversify 機能が増えるほど、コードベースは必然的に大きく複雑になるだろうと考えました。また、複数のオプションをサポートする予定だったので、実行時に動く実装も変わりうると見ていました。 そのため機能間の依存関係が複雑になりそうだと考え、悩んだ末に依存性注入(DI)のためのIoCコンテナを探すことにしました。私はinversifyを選びましたが、もう少し軽量に使いたいのであれば、typediという選択肢もあります。 json-schema-to-typescript 事実上、code generatorの中核となるライブラリです。Code generatorのほぼすべての実装は、このライブラリから望む結果を得るためのオブジェクトを作る作業だと言っても過言ではありません。例えば、次のようなJSON schemaがある場合、 { "title": "Example Schema", "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" }, "age": { "description": "Age in years", "type": "integer", "minimum": 0 }, "hairColor": { "enum": ["black", "brown", "blue"], "type": "string" } }, "additionalProperties": false, "required": ["firstName", "lastName"] } 次のように型を生成します。 export interface ExampleSchema { firstName: string; lastName: string; /** - Age in years */ age?: number; hairColor?: 'black' | 'brown' | 'blue'; } lodash object-traversal js-yaml オプションをYAML形式で受け取るつもりでインストールしました。今思えば、普通にJavaScriptで受け取っても問題なかったと思います。 jsonschema Code generatorが受け付けられるオプションを検証するために選びました。オプション関連のコードがif ~ elseだらけになるのを避けたかったのです。 2. ビルド、デプロイ TypeScript tsup esbuildベースのビルドツールです。速くて設定が簡単なので選びました。 Release Please ライブラリではありませんが、このGitHub Actionのおかげで手軽にデプロイできたので紹介します。 コミットメッセージの中にconventional commit形式で書かれたものがあれば、pull requestの作成からデプロイまで自動で行ってくれます。特にchange log、release、git tagなども自動で管理してくれるので、開発者はコードだけに集中できます。 ちなみに、Inflabのフロントエンドチームでは、monorepoで構成されたパッケージはchangesetsで、単独のリポジトリで運用しているパッケージはRelease Pleaseで管理しています。 commitlint 上で説明したRelease Pleaseの動作の仕組み上、開発者のミスを防ぐために導入しました。コードの変更がコミットされる前にコミットメッセージを検査し、conventional commitの形式に従っていなければコミットできないようにします。 コミットメッセージの一貫性を保ち、デプロイのためにコードを変更したのに、コミットメッセージの誤りでパッケージがデプロイされないという問題を防ぐのが狙いです。 3. テスト Jest 3. 実装の詳細 Code generatorは、大きく3つの段階に分けて実行されます。1つ目は前処理の段階です。正しいオプションが渡されたかを検証し、OASのJSONファイルを読み込みます。続いて、ユーザーが望む前処理ロジック(Pre process)を実行して、読み込んだOAS JSONを加工します。 2つ目の段階では、Taskと呼ばれる作業単位が並列に実行されます。どのタスクを実行するかは、そのタスクのオプションが正しく入力されているかどうかによって決まります。 ReactQueryTaskでは、前処理済みのOAS JSONから生成物に使うオブジェクトを作り、適切な名前と対応付けます。Query、url params、response、request bodyなど、必要と思われる要素をすべて抽出し、型とhooksに変換します。 TypeGuardTaskでは、ReactQueryTaskで作られたインターフェースを検証するtype guard関数を作ります。 SchemaValidatorTaskは、OAS JSONで宣言されたすべてのschemaを検証できるstandaloneのAjvインスタンスを生成します。Ajvについては、後ほど改めて詳しく紹介します。 Taskの並列実行が終わったら、最後の段階として、指定された場所へファイルを書き出します。フローチャートで表すと、次のようになります。 1. オプションの入力を受け取る ユーザーから希望するオプションをYAML形式で受け取るように構成しました。そのオプションは次のとおりです。 loader: fetch | file [required] schema: string [required] output-dir: string [required] react-query: [optional] output: string [required] fetcher: string [required] version: 3 | 4 [optional] default: 3 validator: [optional] output: string [required] typeguard: [optional] output: string [required] preprocess: [optional] filterDeprecated: boolean [optional] default: false extractEnum: boolean [optional] default: false apiPaths: - path: string [required] exact: boolean [optional] tags: - string [required] 2. 前処理 1. オプションの検証 YAMLファイルの有無の確認、node argumentsの入力チェック、kebab-caseのオプションのcamelCaseへの変換など、I/Oと副作用はConfigProviderが担当するようにしました。 一方、react-query、preprocessなど各オプションのパース、検証、値の設定は、ConfigLoaderを実装したオブジェクトが担います。各オブジェクトがパースしたオプションは、ConfigProviderに渡されます。こうしたのは、オプションごとの変更の影響を互いに切り離すためです。 例えば、react queryを担当するクラスは次のように実装されています。 import Ajv from 'ajv'; import type ConfigLoader from './ConfigLoader'; import type { JSONSchemaType } from 'ajv'; import { isNil } from 'lodash'; import { InvalidConfigException } from '../utils/exception'; type ReactQueryConfigOptions = { output: string; fetcher: string; version?: number; removeApiEndpointPrefix?: boolean; }; type YamlReactQueryConfig = { reactQuery?: ReactQueryConfigOptions; }; /** - 1. react-queryオプションのjson validationロジック */ const schema: JSONSchemaType<YamlReactQueryConfig> = { type: 'object', properties: { reactQuery: { type: 'object', nullable: true, properties: { output: { type: 'string' }, fetcher: { type: 'string' }, version: { type: 'number', nullable: true, default: 4, minimum: 3, maximum: 4 }, removeApiEndpointPrefix: { type: 'boolean', nullable: true }, }, required: ['output', 'fetcher'], }, }, additionalProperties: true, }; const validate = new Ajv({ useDefaults: true }).compile(schema); export type ReactQueryConfig = ReactQueryConfigOptions | false; export default class ReactQueryConfigLoader implements ConfigLoader<ReactQueryConfig> { /** - 2. react-queryのオプションの検証 */ load(optionCandidate: unknown) { if (!validate(optionCandidate)) { throw new InvalidConfigException('ReactQueryConfigLoader: not a react query config'); } /** - 3. react-queryのオプションを返す - 戻り値の型はReactQueryConfigOptions | false */ if (isNil(optionCandidate.reactQuery)) { return false; } return optionCandidate.reactQuery; } } ReactQueryConfigLoaderでパースしたオプションは、ConfigProviderのパブリックメソッドとして提供されます。 export class ConfigProviderImpl implements ConfigProvider { private loader: Loader; private options: Options; constructor() { /** - 1. yamlファイルの読み込み */ const yamlCandidate = this.loadConfig(); const reactQueryConfigLoader = new ReactQueryConfigLoader(); /** - 2. react-queryのオプションのパースを依頼 */ const reactQueryConfig = reactQueryConfigLoader.load(yamlCandidate); this.options = { reactQuery: reactQueryConfig, }; } private loadConfig() { const flags = args.parse(process.argv); const yamlPath = resolve(process.cwd(), flags.config); if (typeof yamlPath !== 'string') { throw new InvalidPathException('ConfigProvider: failed to open yaml config file'); } const yamlConfig = camelizeKeys(yaml.load(readFileSync(resolve(__dirname, yamlPath), 'utf-8'))); return yamlConfig; } getOptions(): Options { return this.options; } /** - 3. オプションが必要なオブジェクトにオプションを提供 */ taskOption(): TaskOption { return pick(this.options, ['reactQuery']); } } この構造のおかげで、ConfigLoaderを実装するだけでConfigProviderから使えるようになります。つまり、オプションはいつでも柔軟に追加・変更できます。 2. OpenAPI specificationファイルを受け取る 当初の目標の中に、「ファイルまたはネットワークリクエストでOAS JSONを直接パースし、望む形の結果をファイルとして生成できること。」がありました。これを実現するために、Loaderを実装したFileLoaderとFetchLoaderを作り、inversifyのfactory関数を使って、オプションに応じて使うべきローダーを選択するようにしました。 // inversify.config.ts const container = new Container(); /** - 1. IoCコンテナにオブジェクトを登録 */ container.bind<Loader>(TYPES.Loader).to(FetchLoader).whenTargetNamed('fetch'); container.bind<Loader>(TYPES.Loader).to(FileLoader).whenTargetNamed('file'); /** - 2. ファクトリー関数の設定 */ const loaderFactory = container .bind<interfaces.Factory<Loader>>(TYPES.LoaderFactory) .toFactory<Loader, ['file' | 'fetch']>((context) => { return (named) => { /** - 3. どのオブジェクトをどのように読み込むかを明示 */ return context.container.getNamed<Loader>(TYPES.Loader, named); }; }); // loader.ts const selectedLoader = container.getNamed(TYPES.LoaderFactory, options.loader); このように実装すると、既存のコードを修正せずに別のLoaderを追加できるというメリットがあります。例えば、databaseからOAS JSONを読み込む必要がある場合は、次のように対応できます。 class DatabaseLoader implements Loader { // ... } container.bind<Loader>(TYPES.Loader).to(DatabaseLoader).whenTargetNamed('db'); const databaseLoader = container.getNamed(TYPES.LoaderFactory, 'db'); 3. Preprocess pipe Preprocess pipeは、読み込んだOAS JSONをさまざまな条件で加工する工程です。DeprecatedなAPIを除外したり、特定のAPIだけを選択したり、enumの抽出を選択したりといった処理を担当します。preprocessのオプションはいろいろ選べ、こちらもオプションに対応する前処理のロジックが実行時に決まります。PreprocessPipeクラスが、先ほど紹介したfactory関数でPreprocessを実装したクラスを選んで配列に入れ、OAS JSONを順番に渡しながらロジックを実行します。サンプルコードは次のとおりです。 import { flow } from 'lodash'; class PreprocessPipe { private readonly preprocessOptions: PreprocessConfig; private readonly initialPreprocess: Preprocess[]; constructor( @inject(TYPES.PreprocessFactory) private readonly preprocessFactory: (name: string) => Preprocess, ) {} exec(swaggerSpecJSON: SwaggerSpecJSON | undefined): SwaggerSpecJSON | undefined { /** - 1. オプションで選択されたpreprocessの実装だけを選んで配列に入れる */ const preprocess = this.scheduledPreprocess(); const preprocessorFunctions = preprocess.map((preprocess) => preprocess.exec.bind(preprocess)); /** - 2. preprocessの実装を順番に実行 */ const result = flow(preprocessorFunctions)(swaggerSpecJSON); return result; } private scheduledPreprocess() { if (!this.preprocessOptions) { return []; } /** - enabled = trueのオプションのオブジェクトだけを読み込む */ const selectedPreprocess = Object.entries(this.preprocessOptions) .filter(([, enabled]) => enabled) .map(([name]) => this.preprocessFactory(name)); return this.initialPreprocess.concat(selectedPreprocess); } } こちらも、オプションを追加・削除しても既存のロジックに影響しないように実装しています。オプションの読み込みと検証、OAS JSONファイルの読み込み、preprocessの実行という前処理の段階は、使う場所によって実行のされ方が変わりうるため、できるだけ柔軟に設計しました。 3. ReactQueryTask 上で説明したとおり、json-schema-to-typescriptというライブラリで望む生成物を得るためにオブジェクトを作る工程です。ある意味で最も重要な部分なので、責務を細かく分けることに気を配りました。基本的な流れは次のとおりです。 Generator役のオブジェクトは、Parser役のオブジェクトに必要な部分のパースを依頼します。Parserが結果を返すと、それぞれの役割に応じて生成物を文字列として組み立てます。 Object Generatingを担当するオブジェクトは、パースされたデータを使って適切な形のオブジェクトを作ります。インターフェースとして生成される名前を指定して、json-schema-to-typescriptに渡します。例えば次のとおりです。 import { compile } from 'json-schema-to-typescript'; const queryString = queryStringParser.generate(oasJson); /** - { - "title": "V1UserListQueryString", - "type": "object", - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - } - } - } */ const result = await compile(queryString); /** - export interface V1UserListQueryString { - firstName?: string; - lastName?: string; - } */ react-queryで使うhooksを作るオブジェクトも、同じくパースされたデータから適切な形のhooksの文字列を作ります。Hooksの場合は、template literalを使えば生成結果を簡単に宣言できます。 // template.ts export default function useMutationTemplate(name: string, method: string) { const type = ${name}Response; const optionType = UseMutationOptions<${name}Response, unknown, ${name}Variables, TContext>; return `export function use${name}Mutation<TContext>({ url, options, requestOptions, }: { url: string; options?: ${optionType}; requestOptions?: RequestOptions; }) { const result = useMutation<${type}, unknown, ${name}Variables, TContext>( (variables) => fetcher.${method}(url, variables, requestOptions), options, ) return { ...result, data: result.data?.data }; }`; } // ReactMutationGenerator.ts const result = useMutationTemplate('V1UserList', 'POST'); このようにgenerate layerで作られた結果は、それぞれstring[]の形でReactQueryTaskに戻されます。ここまでの説明は長くてピンとこないかもしれませんが、次のダイアグラムを見ていただければ、思ったよりシンプルな構造であることがおわかりいただけると思います。 4. TypeGuardTask 1. Type guardとは何か TypeScriptにおけるtype guardとは、複数の型を取りうる変数について、型の推論範囲を絞り込む(narrowing)ための関数のことです。例えば次のとおりです。 // type guard const isString = (value: unknown): value is string => typeof value === 'string'; const numOrStringDivide = (value: string | number) => { if (isString(value)) { return parseInt(value, 10) / 2; // value -> string } return value / 2; // value -> number }; フロントエンドでtype guardが必要になるのは、上の例のようにprimitive typeを区別したい場合もありますが、API responseの違いを見分けて処理を分岐させなければならない場面が出てくるからです。特に、通常の関数だけでなくcomponentの中でも便利です。例えば、API responseの形に応じてレンダリングするcomponentを切り替えたい場合です。こうした場面で、Inflabのフロントエンドチームはtype guardを積極的に使っています。 const isStudent = (value: Student | Teacher): value is Student => { return isPlainObject(value) && 'grade' in value && typeof value.grade === 'string'; }; export function Profile() { // Student | Teacher const { data } = useProfileQuery(); return ( <div>{isStudent(data) ? <StudentProfile data={data} /> : <TeacherProfile data={data} />}</div> ); } 2. Type guardの落とし穴 もちろん、GraphQLを使えば__typenameというプロパティを使って簡単に区別できます。しかし、そうでない場合は一般的に、API responseの形を必要な分だけパースする方法で型を区別することになります。上の例で言えば、ある値がオブジェクトで、'grade'というstring型のプロパティを持っていれば、'Student'型とみなすことになります。 これはTypeScriptの構造的型システム(structural type system)に起因するもので、よくまとまったドキュメントが多いので、ここではこれ以上説明しません。 本題に戻ると、Inflabのフロントエンドチームではtype guardを積極的に使っていますが、メリットと同じくらいデメリットもはっきりしていました。それは、type guard自体が誤って宣言されている場合、ロジック上の誤りが表に出てこないということです。代表的なのが、API responseが変わったのにtype guardを変更し忘れたケースです。 上の'Student'型で言えば、API responseが変更されて'grade'というプロパティの型が変わったり、プロパティ自体がなくなったりするケースが考えられます。こうなると、コードレビューをしても見つけにくいだけでなく、コンパイル時には何のエラーも発生しないため、ランタイムで問題が発覚することが多いです。 3. Type guardを自動で作る こうした理由から、type guardもOAS JSONから自動で作られたらどうだろう? というアイデアが浮かびました。同僚と議論した結果、jsonschemaを使ってOASのschemaを検証できる形にし、ファイルとして出力する機能を実装することに決めました。実装全体の流れは次のとおりです。 まず、OAS JSONをJSON schemaの形に変換し、JSON schemaがサポートしていないプロパティを取り除きます。 import { draft7 } from 'json-schema-migrate'; import { unset } from 'lodash'; function convertSwaggerSpecToJSONSchema(swaggerSpec: SwaggerSpecJSON) { const componentSwaggerSpecs = swaggerSpec.components.schemas; /** - 1. OAS -> json schemaの形に変換 */ draft7( toJsonSchema(swaggerSpec.components.schemas, { strictMode: true, }), ); /** - 2. json schemaでサポートされていないプロパティを削除 */ removeUnSupportProperty(componentSwaggerSpecs); return componentSwaggerSpecs; } function removeUnSupportProperty(componentSwaggerSpecs: Record<string, JSONSchema4>) { const notSupportedType = ['default', 'deprecated', 'nullable', 'tsEnumNames', 'example']; traverse(componentSwaggerSpecs, ({ parent, key }) => { key ??= ''; if (notSupportedType.includes(key)) { unset(parent, key); } }); } こうして作ったJSON schemaを、template関数を使って文字列にすれば完成です。 const defaultTypeGuardTemplate = (jsonSchemas: Record<string, JSONSchema4>) => { return [ importTemplate(Object.keys(jsonSchemas)).trim(), 'const validator = new Validator();', Object.entries(jsonSchemas) .map(([key, schema]) => typeGuardTemplate(key, schema)) .join('') .trim(), ].join('\n\n'); }; const importTemplate = (keys: string[]) => { return ` import { Validator } from 'jsonschema'; import type { ${keys.join(', ')} } from './schema'; `; }; const typeGuardTemplate = (type: string, jsonSchema: JSONSchema4) => { const stringifyJsonScheme = JSON.stringify(jsonSchema); return ` export const ${is${type}} = (value: unknown): value is ${type} => { return validator.validate(value, ${stringifyJsonScheme}).valid; } `; }; const schema = convertSwaggerSpecToJSONSchema(oasJson); const result = defaultTypeGuardTemplate(schema); まだ完璧に動作するわけではありませんが、問題なく使えるようドッグフーディングを続けています。 5. SchemaValidatorTask 上でもお伝えしたとおり、Elixirのdoc testの考え方を取り入れ、バックエンドエンジニアが自分の宣言したOASと実際のresponseが一致しているかを、integration testの段階で検証できる機能を提供しようとしました。ライブラリにはAjvを選びました。バックエンド側で別途ライブラリをインストールしなくても、standaloneモードで動作するからです。 SchemaValidatorTaskの仕組みは、前の2つのタスクに比べてとてもシンプルです。OASをJSON schemaに変換し、standaloneモードを適用した生成物を返せば完了です。 import toJsonSchema from '@openapi-contrib/openapi-schema-to-json-schema'; import Ajv from 'ajv'; import standaloneCode from 'ajv/dist/standalone'; const schema = toJsonSchema(oasJson); const ajv = new Ajv({ schemas, code: { source: true, optimize: true, }, }); const result = standaloneCode(ajv); 6. 生成物の出力 各タスクでは、受け取ったstring配列から一定の基準で重複を取り除きます。ここまで問題がなければ、オプションで指定されたパスに結果を出力します。 4. 生成物の形 次のようなOAS JSONを受け取ったと仮定すると、各タスクの実行結果は以下のとおりです。 OAS.json{ "openapi": "3.0.0", "paths": { "/api/v1/category": { "get": { "operationId": "CategoryB2cController_findAll", "summary": "すべてのカテゴリー一覧", "description": "すべてのカテゴリー一覧を取得する。", "parameters": [], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "properties": { "statusCode": { "type": "string" }, "message": { "type": "string" }, "data": { "$ref": "#/components/schemas/CategoryAllResponse" } } } } } } }, "tags": ["カテゴリーenum API"] } } }, "components": { "schemas": { "CodeName": { "type": "object", "properties": { "code": { "type": "string" }, "name": { "type": "string" } }, "required": ["code", "name"] }, "JobGroupCodeName": { "type": "object", "properties": { "code": { "type": "string" }, "name": { "type": "string" }, "jobs": { "type": "array", "items": { "$ref": "#/components/schemas/CodeName" } } }, "required": ["code", "name", "jobs"] }, "CategoryAllResponse": { "type": "object", "properties": { "jobGroup": { "type": "array", "items": { "$ref": "#/components/schemas/JobGroupCodeName" } }, "jobSeekerStatus": { "type": "array", "items": { "$ref": "#/components/schemas/CodeName" } } }, "required": ["jobSeekerStatus"] } } } } ReactQueryTaskimport { useQuery, useMutation } from '@tanstack/react-query'; import type { UseQueryOptions, UseMutationOptions, QueryKey } from '@tanstack/react-query'; import fetcher from './Fetcher'; import type { RequestOptions } from './Fetcher'; export type Maybe<T> = T | null; export type OneOf<T> = T extends unknown[] ? T[number] : T extends readonly unknown[] ? T[number] : T; export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]-?: Exclude<T[K], undefined>; }; export type Values<T extends Record<string, unknown>> = T extends Record<string, infer U> ? U : T extends { [key: string]: infer U } ? U : never; export const enum ResponseStatus { OK = 'OK', SERVER_ERROR = 'SERVER_ERROR', DUPLICATE = 'DUPLICATE', NOT_FOUND = 'NOT_FOUND', BAD_REQUEST = 'BAD_REQUEST', BAD_PARAMETER = 'BAD_PARAMETER', CONFLICT = 'CONFLICT', UNAUTHORIZED = 'UNAUTHORIZED', FORBIDDEN = 'FORBIDDEN', PAYLOAD_TOO_LARGE = 'PAYLOAD_TOO_LARGE', TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS', } export type Success<T = unknown> = { statusCode: ResponseStatus.OK; message: ''; data: T; }; export type Error = { statusCode: Omit<keyof typeof ResponseStatus, 'OK'>; message: string; data: Maybe<''>; }; /** - @typeParam T - Response data type */ export type ApiResponse<T = unknown> = Success<T>; export const API_ENDPOINT = { V1_CATEGORY: '/api/v1/category', } as const; export const fetchV1Category = async (url: string, requestOptions?: RequestOptions) => fetcher.get<V1CategoryResponse>(url, requestOptions); export function useV1CategoryQuery({ queryKey, url, options, requestOptions, }: { queryKey: QueryKey; url: string; options?: Omit< UseQueryOptions<V1CategoryResponse, unknown>, 'queryKey' | 'queryFn' | 'initialData' > & { initialData?: () => undefined; }; requestOptions?: RequestOptions; }) { const result = useQuery<V1CategoryResponse, unknown>( queryKey, () => fetchV1Category(url, requestOptions), options, ); return { ...result, data: result.data?.data }; } /** - created by ResponseSchemaGenerator */ export interface V1Category { data: Maybe<CategoryAllResponse>; } export interface CategoryAllResponse { jobGroup: Maybe<JobGroupCodeName[]>; jobSeekerStatus: CodeName[]; } export interface JobGroupCodeName { code: string; name: string; jobs: CodeName[]; } export interface CodeName { code: string; name: string; } export type V1CategoryResponse = ApiResponse<V1Category['data']>; TypeGuardTaskimport { Validator } from 'jsonschema'; import type { CodeName, JobGroupCodeName, CategoryAllResponse } from './schema'; const validator = new Validator(); export const isCodeName = (value: unknown): value is CodeName => { return validator.validate(value, { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' } }, required: ['code', 'name'], }).valid; }; export const isJobGroupCodeName = (value: unknown): value is JobGroupCodeName => { return validator.validate(value, { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' }, jobs: { type: 'array', items: { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' } }, required: ['code', 'name'], }, }, }, required: ['code', 'name', 'jobs'], }).valid; }; export const isCategoryAllResponse = (value: unknown): value is CategoryAllResponse => { return validator.validate(value, { type: 'object', properties: { jobGroup: { type: 'array', items: { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' }, jobs: { type: 'array', items: { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' } }, required: ['code', 'name'], }, }, }, required: ['code', 'name', 'jobs'], }, }, jobSeekerStatus: { type: 'array', items: { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' } }, required: ['code', 'name'], }, }, }, required: ['jobSeekerStatus'], }).valid; }; SchemaValidatorTaskの結果は1つの複雑なファイルになるため、ここでは紹介を割愛します。 こうして作られた生成物は、フロントエンドのコードで次のように使えます。 import { useV1CategoryQuery, API_ENDPOINT } from './schema'; import { isJobGroupCodeName } from './guard'; const retrieveJobGroupCodeNameJobs = (value: unknown) => { if (isJobGroupCodeName(value)) { return value.jobs; } return []; }; export function A() { const { data } = useV1CategoryQuery({ url: API_ENDPOINT.V1_CATEGORY, queryKey: [API_ENDPOINT.V1_CATEGORY], }); return <div>{data.jobSeekerStatus.map((status) => status.name)}</div>; } バックエンドのテストコードでは、schema validatorを次のように使えます。 import validator from './validator'; describe('categoryAllResponse', () => { it('categoryAllResponseのOASと実際の結果は一致しなければならない。', async () => { const response = await controller.categoryAllResponse(); const result = validator.categoryAllResponse(result); expect(result).toBe(true); }); }); 5. まとめ 最初にお伝えしたとおり、一見すると工程は複雑に見えますが、まったくそんなことはありません。むしろ実装中に悩んだのは、責務をどう適切に分担するかと、SonarQubeのduplicate line code smellを避けるためにコードの重複をどう抑えるかでした。そのため、コード全体を通して次のような選択をしました。 各オブジェクトの責務を細かく分離するよう努めました。 各オブジェクトの横断的関心事はServiceというlayerに分け、必要なオブジェクトが必要な分だけ参照するように実装しました。 実行時に動くロジックを切り替える必要がある場合は、Strategyパターンを積極的に使いました。 同じロジックで小さな部分だけを変える必要がある場合は、Template Methodパターンを使いましたが、継承が1段階を超えないように実装しました。 実装した構造を簡単に表すと、次のようになります。 実装の紹介は以上です。 次回の記事では、code generatorを導入してから働き方がどのように変わったのかをご紹介します。

  6. YouTube · · YouTube description · Eddwan Hallen · egoist/mygo

    Open source do GitHub: #15 a #11 | Vol. 16 #shorts Os projetos #15 a #11 do Top 15 open source do GitHub, Vol. 16, 10 segundos para cada um. #15 SOMA: Kai-Jungsthoefel/SOMA #14 Rune: unstablebuild/rune #13 MyGo: egoist/mygo #12 Solid Glass: deeplethe/solid-glass #11 anydoc: firecrawl/anydoc Vídeo completo com os 15: youtu.be Oferecimento: Hallen Tech, hallentech.com #shorts #opensource #github

    10 likes · 190 views
  7. YouTube · · YouTube description · Eddwan Hallen · egoist/mygo

    Mais 15 projetos open source do GitHub que valem o seu tempo | Vol. 16 Mais 15 projetos open source do GitHub que valem o seu tempo, em contagem regressiva e com 10 segundos para cada um: o iPhone ajudando o Mac a rodar um modelo de IA, livros que viram aulas animadas, CAD feito pelo agente, 20 anos do Hacker News offline e muito mais. 📺 Décimo sexto volume da série Open source do GitHub. Escolhidos pela análise da lista inteira (estrelas, frequência de commits, último commit, README, documentação, capturas e site), com licença e atividade conferidas em 26/10/2026. Qual desses você vai testar primeiro? Conta nos comentários! ⏱️ CAPÍTULOS E LINKS 0:00 Abertura 0:10 #15 SOMA: CAD em que o agente de código escreve o modelo 3D em Python (CadQuery) e você revisa, mede e exporta para imprimir Kai-Jungsthoefel/SOMA 0:20 #14 Rune: IDE e multiplexador de terminal acelerados pela GPU, com rede criptografada entre as suas máquinas (1,3 mil estrelas) unstablebuild/rune 0:30 #13 MyGo: apps de desktop em Go com interface nativa ou web, num único binário de poucos megabytes e sem cgo (3,0 mil estrelas) egoist/mygo 0:40 #12 Solid Glass: design system com estética de vidro sólido para aplicações de IA, com componentes e telas prontas (2,2 mil estrelas) deeplethe/solid-glass 0:50 #11 anydoc: biblioteca em Rust da Firecrawl que converte Word, PowerPoint, Excel, PDF, EPUB e outros em Markdown limpo (23,1 mil estrelas) firecrawl/anydoc 1:00 #10 Backburner: ligue o iPhone no Mac pelo USB-C e ele ajuda a rodar o Qwen de 27B: leitura de prompt mais rápida e mais contexto (1,1 mil estrelas) StayLameBro/backburner 1:10 #9 Shimmer From Structure: gera a animação de carregamento (skeleton) medindo o layout real, em React, Vue, Angular, Svelte e SolidJS (1,1 mil estrelas) darula-hpp/shimmer-from-structure 1:20 #8 Mural: app nativo de iPhone e Android para aprender idiomas conversando por voz, com o progresso guardado no aparelho (1,7 mil estrelas) Chuloo/mural 1:30 #7 HackerBook: 20 anos do Hacker News num arquivo estático que roda inteiro no navegador, offline, atualizado toda semana DOSAYGO-STUDIO/HackerBook 1:40 #6 Papermorph: skill do Claude Code que transforma livros didáticos em aulas narradas e interativas, e histórias em filmes de papel DozenTwelve/Papermorph 1:50 #5 ftop: monitor do sistema para Macs com Apple Silicon numa janela de vidro flutuante: CPU, memória, rede e quem está pesando Nongfsq/ftop 2:00 #4 Renewlet: controle self-hosted de assinaturas: quanto você gasta, o que vence e lembretes antes da renovação zhiyingzzhou/renewlet 2:10 #3 Claude Octopus: plugin do Claude Code que chama até doze provedores externos para revisar o trabalho e avisa quando eles discordam (4,3 mil estrelas) nyldn/claude-octopus 2:20 #2 plMail: cliente de e-mail self-hosted com calendário, que junta IMAP, Gmail e Outlook numa caixa só karatektus/pl_mail 2:30 Oferecimento Hallen Tech 2:44 #1 Company Brain: colega de IA no Slack que lembra das decisões do time e faz o trabalho: abre issues, lê PRs e mexe no repositório (antes era produto pago da Supermemory) (1,1 mil estrelas) supermemoryai/company-brain 2:54 Encerramento 🧡 Oferecimento: Hallen Tech, liderança de tecnologia sob demanda, software, cloud e segurança para a sua empresa. Site: hallentech.com E-mail: contato@hallentech.com Instagram: instagram.com LinkedIn da Hallen Tech: linkedin.com LinkedIn do Eddwan: linkedin.com Se curtiu, deixa o like, segue o canal e ativa as notificações para a próxima lista. #opensource #github #programacao #ferramentas

    293 likes · 4 comments · 11,211 views
  8. YouTube · · YouTube description · MakSim-IT · egoist/mygo-cloudmusic

    Топ-10 GitHub за 3 дня: tstack, GEOInflow Обзор: топ-10 новых репозиториев GitHub за 24–27 октября 2026 — open-source проекты, которые быстрее всех набрали звёзды. В выпуске: навыки для агентов Claude Code и Codex; аналитика сайта и обходов ИИ-краулеров; бэкенд-фреймворк на Lean с доказанной безопасностью доступа. 1. t3dotgg/tstack — Набор навыков для агентов Claude Code и Codex: PR от проверки до слияния. (Python, +1672 ★) 2. yaojingang/GEOInflow — Своя аналитика сайта и обходов ИИ-краулеров для GEO, v1.2. (TypeScript, +433 ★) 3. NyxFoundation/nyxis — Бэкенд-фреймворк на Lean с доказанной безопасностью доступа. (Lean, +271 ★) 4. mochiiwuff/capri-desktop — Капри из «Киберпанк: Бегущие по краю 2» на рабочем столе комментирует Claude Code. (JavaScript, +257 ★) 5. blakeandersonw/ai-app-playbook — Пошаговый план создания прибыльного приложения в 2026 году. (TypeScript, +249 ★) 6. heydeer/pngdeer — Сжатие PNG с потерями: само подбирает качество, без ползунка. (Rust, +205 ★) 7. dlt2000/xc-soundfont-synth — Компонент SoundFont (SF2) для самодельных инструментов на ESP-IDF. (C, +198 ★) 8. piermark/omarchytv — Домашний экран для Android TV в стиле Omarchy: 26 тем и MCP-сервер. (Java, +188 ★) 9. Bodila51/reverse-engineer-Wi-Fi-router — Навык Claude Code: по фото роутера находит его админ-API и проверяет приватность. (Python, +178 ★) 10. egoist/mygo-cloudmusic — Сторонний нативный клиент NetEase Cloud Music на Go, без WebView. (Go, +177 ★) Источник: GitHub Search API, репозитории созданы с 2026-10-24, отсортированы по звёздам. Лидер: t3dotgg/tstack Остальные: yaojingang/GEOInflow, NyxFoundation/nyxis, mochiiwuff/capri-desktop, blakeandersonw/ai-app-playbook, heydeer/pngdeer, dlt2000/xc-soundfont-synth, piermark/omarchytv, Bodila51/reverse-engineer-Wi-Fi-router, egoist/mygo-cloudmusic #GitHub #opensource #программирование Каждые 3 дня — 10 новых open-source проектов GitHub, быстрее всех набравших звёзды. Подписывайся, чтобы не пропустить следующий выпуск.

    3 views
  9. V2EX · · 分享发现 · @SayHelloHi · egoist/mygo

    使用 Go 开发桌面 App 最近用这个项目开发了一个跨平台的 App 体验不错 项目 egoist/mygo 真实项目 语音输入法 arion.astrafoundry.si 音乐 App music.egoist.dev ![]( i.imgur.com)

    2 comments
  10. Bluesky · · @umeru.ma · egoist/kero

    v2直近15時間ぶっ通しで開発して作ったっぽいな。Opus5.5と一緒に。 egoist/kero: A native terminal workspace for macOS. egoist/kero

Achievements

Awards earned by the repositories egoist owns. Select an award to embed its badge.

Gold
01st place finishes
Silver
12nd place finishes
Bronze
13rd place finishes
Awarded repositories
1Owned by egoist now

Repositories

Ordered by gold, then silver, then bronze.

  1. hackBest: 2nd by stars, 357 stars011

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. hack
  2. hack

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.