背景
由于个人兴趣,我需要一个dart语言的命令行补全建议库,当前最权威的就是 fig 的—— withfig/autocomplete , 但它是由 javascript 语言编写的。 想把它引入到 flutter App项中,正值春节大假,有时间盘一盘这个项目。
如果你还不知道这个项目是干啥的,可以看看下面的介绍
- docs.aws.amazon.com/amazonq/lat… 被aws收购后,并入了 Amazon Q工具。
- juejin.cn/post/711912…
- zhuanlan.zhihu.com/p/492181067
- www.bilibili.com/video/BV1NR… 视频介绍
一句话
我把 github.com/withfig/aut… 这个 25k star 的开源ts代码,使用dart实现了,开源在 github.com/littlewrite… ,并应用到了 www.faiterm.com/ 终端App中。
意义
接下来的内容,你将了解到:
- 一个核心问题:为何通用转换工具(如 ts2dart)在面临大型、复杂的 TypeScript 项目时会“失灵”。
- 一种混合策略:如何通过“观察规律 + 编写专用转换器 + AI 辅助修补”的三段式方法,系统性地处理、翻译海量代码。
- 一次实战优化:从一次加载 500MB 内存的 MVP,到通过延迟加载、缓存策略实现平滑集成的完整演进路径。
- 一类典型问题的解法:在静态类型语言(Dart)中,如何模拟动态语言(JavaScript)的特性(如动态导入),并平衡内存、性能与开发体验。
一期工程
先看看一共有多少代码。 这个项目我先肉眼预览了一遍,代码量还是比较大的,有1.4k +个文件,代码行数: 2百万+ 。
$ cd autocomplete/src
$ find . -name "*.ts" -type f | wc -l
1484 # 个 ts 文件
$ find . -type f -name "*" -exec wc -l {} + | tail -1
2281947 total # ts 代码行数
代码量还是挺大的。
ts2dart ?
用代码转换? 我尝试过 [ts2dart]](github.com/dart-archiv…) 这个开源项目来处理代码。处理结果是:
...
^^^^^
clang.ts:4863:3: Property assignment expected.
clang.ts:4864:10: ';' expected.
clang.ts:4865:1: Expression expected.
处理简单的ts代码还行,稍微复杂点的就会报错,例如这个 clang.ts, 它包含一些 ts 特性,dart 又没有实现的就乱转了。 这个项目已归档不再更新。
给大家看看转换结果:
final List < Fig . Suggestion > stdCSuggestions = [ { "name" : [ "c89" , "c90" , "iso9899:1990" ] , "description" : "ISO C 1990" } , { "name" : "iso9899:199409" , "description" : "ISO C 1990 with amendment 1" } , { "name" : [ "gnu89" , "gnu90" ] , "description" : "ISO C 1990 with GNU extensions" } , { "name" : [ "c99" , "iso9899:1999" ] , "description" : "ISO C 1999" } , { "name" : "gnu99" , "description" : "ISO C 1999 with GNU extensions" } , { "name" : [ "c11" , "iso9899:2011" ] , "description" : "ISO C 2011" } , { "name" : "gnu11" , "description" : "ISO C 2011 with GNU extensions" } , { "name" : [ "c17" , "iso9899:2017" , "c18" , "iso9899:2018" ] , "description" : "ISO C 2017" } , { "name" : [ "gnu17" , "gnu18" ] , "description" : "ISO C 2017 with GNU extensions" } , { "name" : "c2x" , "description" : "Working Draft for ISO C2x" } , { "name" : "gnu2x" , "description" : "Working Draft for ISO C2x with GNU extensions" } ] ; final List < Fig . Suggestion > stdCPPSuggestions = [ { "name" : [ "c++98" , "c++03" ] , "description" : "ISO C++ 1998 with amendments" } , { "name" : [ "gnu++98" , "gnu++03" ] , "description" : "ISO C++ 1998 with amendments and GNU extensions" } , { "name" : "c++11" , "description" : "ISO C++ 2011 with amendments" } , { "name" : "gnu++11" , "description" : "ISO C++ 2011 with amendments and GNU extensions" } , { "name" : "c++14" , "description" : "ISO C++ 2014 with amendments" } , { "name" : "gnu++14" ,
没格式化,代码有错误也没法格式化,属于没法用。这个方案看来是行不通的。pass掉
All in AI ?
直接丢给AI?
我担心项目还没抱出来,钱包先爆掉。pass掉
vibe coding: ts 2 dart
我开始观察这个项目,看看有啥规则,规律。
这个项目的文件,代码量很多,但每个文件都有着类似的结构。 例如: ./src/@fig/publish-spec.ts
const completionSpec: Fig.Spec = {
name: "@fig/publish-spec",
description: "Publish a spec to fig teams",
subcommands: [
{
name: "help",
description: "Display help for command",
priority: 49,
args: { name: "command", isOptional: true },
},
],
...
./src/adb.ts
const completionSpec: Fig.Spec = {
name: "adb",
description: "Android Debug Bridge",
subcommands: [
{
name: "devices",
description: "List connected devices",
options: [
{
name: "-l",
description: "Long output",
},
],
},
...
./src/cf.ts
const completionSpec: Fig.Spec = {
name: "cf",
description: "Cloudfoundry cli",
subcommands: [
{
name: ["app", "a"],
description: "Display health and status for app",
args: {
name: "APP_NAME",
description: "The app you want to get health and status for",
generators: generateAppNames,
},
options: [
{
name: "--guid",
description:
"Retrieve and display the given app's guid. All other health and status output for the app is suppressed",
},
],
},
...
每个文件的入口文件都是如此,都是一个spec 对象,对象那包含很多对象数组,很少有函数逻辑(是的有自定义函数),大量的数组,对象定义代码,从ts转换为dart就是语法调整了一下,不容易出错。于是就让AI写了个 Fig.Spec 转换为 Dart FigSpec 的代码,先解决大部分工作。
输入需求, AI 就给出了一个 tools/converter-engine.cjs
怎么使唤AI的流程就不赘述了,直接给结果—— tools/ts-to-dart-converter.cjs 这个是转换代码。
代码转换结果: ./dart/lib/specs/arch.dart
// Auto-generated from TypeScript source: arch.ts
// Generated at: 2026-02-12
// WARNING: Manual changes may be overwritten!
import 'package:autocomplete/src/spec.dart';
/// Completion spec for `arch` CLI
final FigSpec archSpec = FigSpec(
name: 'arch',
description: 'Print architecture type or run select architecture',
parserDirectives: ParserDirectives(
flagsArePosixNoncompliant: true,
optionsMustPrecedeArguments: true
),
options: [
Option(
name: '-32',
description: 'Add the native 32-bit architecture to the list of architectures'
),
Option(
name: '-64',
description: 'Add the native 64-bit architecture to the list of architectures'
),
Option(
name: '-c',
description: 'Clear the environment that will be passed to the command'
),
Option(
name: '-d',
description: 'Delete the named environment variable from the command\'s environment',
isRepeatable: true,
args: [
Arg(
name: 'envname'
)
]
),
Option(
name: '-e',
description: 'Assign the given value to the variable in the command\'s environment',
isRepeatable: true,
args: [
Arg(
name: 'envname=value'
)
]
),
Option(
name: '-h',
description: 'Print a help message and exit'
)
],
args: [
Arg(
name: 'program',
template: 'filepaths',
isVariadic: true
)
]
);
这样在写个 批量脚本就能完成整个项目的转换了,也就废了点写cjs的AI token,很划算,并且js的执行效率也比AI生成快非常快。
这样也只是转换了大部分代码,还有些代码用到了些特别的语法,以及动态函数,无法被转换,别跳过了。
先花 20%的时间,转换80%的代码,跑通MVP!
驱动代码
当前这个 autocomplete 项目中,ts 代码是命令的定义数据,还没法工作。例如,命令解析,load complete 这些功能是没有的。为了大家方便,就不单独拆一个 dart 项目存放了,直接搁一块了。
这里我借鉴了 -- microsoft/inshellisense 的代码,逻辑写到 ./dart/lib/src 目录下了。
ts 和 dart 语法特性问题
在处理 inshellisense 的代码时,发现这么一段代码:
const loadSpec = async (cmd: CommandToken[]): Promise<Fig.Spec | undefined> => {
...
if (specSet[rootToken.token]) {
const specPath = specSet[rootToken.token];
const importPath = path.isAbsolute(specPath) ? pathToFileURL(specPath).href : specPath;
const spec = (await import(importPath)).default;
loadedSpecs[rootToken.token] = spec;
return spec;
}
};
const spec = (await import(importPath)).default;
这里的意思是,当我用到 git 命令时,才加载 git 相关的建议代码,当我用到 cd 代码是,才导入 cd 相关的建议。这个lazy load 是个非常棒的设计。
javascript 是一个动态语言,支持动态 import ,但 dart 不行,当前就只能全部import 了。
我就又让 AI 写了个加载所有 spec的 dart 生成代码——generate-all-specs.cjs
./dart/lib/spec/all_specs.dart
import '../src/registry.dart';
import '-.dart';
import '@capgo/cli.dart';
import 'act.dart';
import 'adb.dart';
import 'adr.dart';
import 'afplay.dart';
...
/// Register every spec. Called by [registerBuiltinSpecs] in autocomplete.dart.
void registerAllSpecs() {
registerSpec(specSpec.name, () => specSpec);
registerSpec(cliSpec.name, () => cliSpec);
registerSpec(figPublishSpecSpec.name, () => figPublishSpecSpec);
registerSpec(presetSpec.name, () => presetSpec);
registerSpec(withfigAutocompleteToolsSpec.name, () => withfigAutocompleteToolsSpec);
registerSpec(wordpressCreateBlockSpec.name, () => wordpressCreateBlockSpec);
registerSpec(actSpec.name, () => actSpec);
...
好,完成了周边的命令解析代码,spec 驱动代码,写个example 试试—— dart/example/run_suggest.dart
void main(List<String> args) async {
String? cmd;
var shell = Shell.bash;
...
registerBuiltinSpecs();
final cwd = Directory.current.path;
final blob = await getSuggestions(cmd, cwd, shell);
if (blob == null) {
print('(no spec or no suggestions)');
exit(0);
}
if (blob.argumentDescription != null) {
print('Argument: ${blob.argumentDescription}');
}
for (final s in blob.suggestions) {
print('${s.name}\t${s.description ?? ""}');
}
print(
'(${blob.suggestions.length} suggestions, charactersToDrop: ${blob.charactersToDrop})');
}
$ dart run ./example/run_suggest.dart "cut "
dart
example
docs
README.md
pubspec.yaml
lib
assets
-b Byte positions as a comma or - separated list of numbers
-c Column positions as a comma or - separated list of numbers
-f Field positions as a comma or - separated list of numbers
-n Do not split multi-byte characters
-d Use delim as the field delimiter character instead of the tab character
-s Suppress lines with no field delimiter characters. unless specified, lines with no delimiters are passed through unmodified
-w Use whitespace (spaces and tabs) as the delimiter. Consecutive spaces and tabs count as one single field separator
(17 suggestions, charactersToDrop: 0)
好,一期工程完毕,完成了 MVP。
二期
当前项目还有这些问题:
- 大部分代码处理,都是一些简单易懂的代码,但是一些复杂的,动态代码是没处理的。
- 有些代码结构也简单,但它的结构有点变化,只是多了个变量、数组,并且也能找到规律,可以加入到通用转换逻辑。
- 需要接入实际的 flutter app 中,看看性能如何,cpu,内存占用如何。
解决方案
我是这样解决的
- 一些 ts2dart.cjs 没法转换的、简短的ts代码,直接丢给 AI,转换完了后,加上 complete 标记,避免 cjs 重复转换已处理好的 dart 代码。
- 对于一些 Fig.Option , 这些数组,这些代码,结构简单,但是往往有几百行,交给AI容易出错,可以写个ts代码来处理 。
- 对于一些常用的命令,自动转换失败的,很大的文件,人工去处理。例如:docker.ts, python.ts, npm.ts, git.ts . 这些命令的使用率很高,同时这些文件也很大,丢给AI,或者ts转dart都不能完全转换为正确的 dart 代码。
我人工review了每个ts文件,把一些简短的ts丢给AI重新生成,并标记 complete。再次 完善 ts2dart.cjs 。
在node中引入了多进程,来加快任务。之前发现了这个执行效率问题,加上了多线程,异步还是没把cpu跑满,想起来js的异步机制,就改为多进程进一步压榨cpu。
提交到仓库后—— littlewrite/autocomplete , 在Faiterm app 引入后使用了下,发现了新的问题,第一次引入时,程序直接卡了一下,然后内存蹦到了500MB。
通过打时间戳分析了一下,卡的那一帧是 all spaces.dart 代码里执行 registry all 导致的,这个函数就是初始化所有 command spec对象,一共几百个对象,注册完内存就上去了。
三期工程
待解决问题:
- 一次性注册所有命令内存,cpu 爆炸
- 不支持主命令推荐。
- 一些常用命令待人工review。
优化
问了下AI,dart 没有动态import,但有 延迟加载 (deferred)。
import 'package:autocomplete/specs/gt.dart' deferred as spec_gt; // 声明但并未实际导入
await spec_gt.loadLibrary(); // 运行时加载与使用
按照上面的技术方案,我调整了 generate-all-specs-v2.cjs .
之前的补全的建议是在明确了命令的情况给出的子命令建议推荐,现在新增命令推荐。原来输入 ”git “ ,”lsof “ 才能建议,现在新增功能,输入 ”g”, “d”, ”c” 时,提供命令推荐。
我先把所有的命令都集合到一个数组里,这样就能实现输入首字母然后推荐命令了。
./dart/lib/specs/all_specs_v2.dart
/// Command names available in this v2 bundle (for command-name completion without loading).
const List<String> v2SpecNames = ['-', '@commercelayer/cli', '@fig/publish-spec', '@forge/cli', '@withfig/autocomplete-tools', '@wordpress/create-block', 'Rscript', 'access-context-manager', 'accessanalyzer', 'account', 'acm-pca', 'acr', 'act', 'active-directory', 'ad', 'adb', ......
然而这个数组很大,通过首字母过滤推荐,我感觉有点慢,并且我似乎也用不到这么大的数组,我又想了个办法,把这些命令按照首字母归类。
// 按照首字母分类的命令映射
final Map<String, List<String>> categorizedCommands = {
'@': [
'@commercelayer/cli',
'@fig/publish-spec',
],
'a': [
'access-context-manager',
'accessanalyzer',
'account',
],
'R': [
'Rscript',
],
这样我就不用加载整个数组了。
...
等等,好像不太对劲,这不还是一个变量么,这个大Map。
我又优化了一下,改为每个字母为一个单独数组
const List<String> v2SpecNamesFirstChar_at = [
'@commercelayer/cli',
'@fig/publish-spec',
...
const List<String> v2SpecNamesFirstChar_a = [
'act',
'adb',
'adr',
...
const List<String> v2SpecNamesFirstChar_b = [
'babel',
'banner',
'barnard59',
...
const List<String> v2SpecNamesFirstChar_d = [
'dart',
'date',
四期
之前的我以为我上传成功了,其实并没有上传完整,它有这样的规则,哪怕你通过git add -f 把文件提交到了仓库,但它会参考.gitignore 中的设置,也就是 dart publish 的流水线会忽略这部分文件,这是个要注意的点!我是通过 pub.dev 的静态检查报错信息里面看到的,它报告找不到某某文件(这些文件虽然在git仓库,但命中了 .gitignore 的规则)
调整了 .gitignore 文件后,这些文件能提交了,再上传时又报错文件太大了。
│ ├── suggestion.dart (7 KB)
│ └── template.dart (2 KB)
└── pubspec.yaml (<1 KB)
Total compressed archive size: 7 MB.
Validating package... (43.5s)
...
Policy details are available at https://pub.dev/policy
Package has 2 warnings and 1 hint.. Do you want to publish autocomplete 0.1.2-dev to https://pub.dev (y/N)? y
Uploading... (9.5s)
Failed to scan tar archive. (Maximum total length reached: 104857600.)
这里展示的信息表示,上传到 pub.dev 过程中,项目压缩后右 7MB,但在服务器端解压后超过 100MB了,不符合 pub.dev 的规则,建议我这么多代码,拆成多个包发布。
再检查了下 dart 项目,有 104MB,算了,找几个不那么常用的命令删减一下吧,🤣
% du -hd 1 | sort -rh
104M .
44M ./aws
25M ./az
20M ./gcloud
436K ./heroku
328K ./fig
96K ./shopify
76K ./infracost
60K ./dotnet
44K ./task
36K ./deno
12K ./php
12K ./example
12K ./@capgo
8.0K ./@withfig
8.0K ./@usermn
4.0K ./python
4.0K ./@wordpress
这三大云的命令,都有很大啊,删一个云是删,那就三个一起删了。
这样就能上传到 pub.dev 了。
第五期
这一期改动都是几周之后了,这个项目完成后,我后续有空也看了看源码,发现当前是函数式的,中间发现解析spec是个一个深度检索,也就是你选择了git 命令后续的检索都是在这个git spec 上进行检索,也就是可以缓存可以避免多次解析 git spec 对象。
例如:你输入命令时,就是这样的 git ,git b,git br ,这会触发三次命令推荐函数,当第一次输入git 时,后续的推荐命令,都是在 git spec 内找,后续就可以服用 git spec 对象,避免了多次检索,如果是函数式,就需要一个全局缓存对象。
按照 dart/flutter 的设计思想,可以改为对象,这个缓存变量作为一个对象属性,和view一样有自己的生命周期管理,dispose 就回收资源。
这样之后,那个全局缓存对象就没有无限增长了,引入这个 autocomplete 后,app 内存,cpu 也没有突刺了。
结束
好的,autocomplete for dart 就算完成了,实装效果,大家下载 Faiterm.app 体验看看,作为这个终端的自动提示,使用起来还是非常棒的, 跨平台开发,Windows,Mac,Linux 的用户都可以使用。
回顾与经验分享:
- 工具是手段,而非银弹:面对 200 万行代码,试图寻找一个全自动的转换工具一劳永逸是不现实的。正确的方法是快速验证工具的边界,然后立即转向能解决 80% 问题的、更定制化的方案。
- 让 AI 做它擅长的事:在这次迁移中,AI 的角色不是“翻译整个项目”,而是“撰写转换规则引擎”和“处理零散的、模式不统一的代码片段”。这极大地降低了成本并提高了准确性。
- 性能问题是设计出来的:初期 500MB 的内存占用是一个强烈的设计信号。它迫使我们将架构从“一次性全量加载”重新思考为“按需加载 + 智能缓存”,这反而是打磨出更健壮、更贴合 Dart/Flutter 生态设计模式(如对象生命周期)的契机。
- 发布即交付,细节定成败:
.gitignore规则和 pub.dev 的包大小限制,这两个看似微小的“发布流水线”细节,最终影响了交付物的内容。工程化项目必须尽早并频繁地走通整个构建、打包、发布流程。