- 编译器
- 编程语言
- 开发工具
【免费下载链接】typescript-go
Staging repo for development of native port of TypeScript
导读
在 Node.js 与 TypeScript 的现代模块体系(node16/nodenext)中,package.json的imports字段允许项目通过#开头的内部别名(subpath import)引用自身源码,例如#/*映射到./src/*。本文以 typescript-go(TypeScript 的 Go 原生移植版,仓库位于typescript-go)仓库中的一份 fourslash 基线测试 autoImportPackageJsonImportsHashSlashNode16.baseline.md 为切入点,完整还原该场景下自动导入(Auto Import)补全从触发、解析到生成模块说明符的完整链路,并对照仓库中的测试源码与语言服务实现,说明#子路径导入在补全、模块说明符选择与偏好设置上的行为细节。读完本文,你将掌握:imports通配符映射的写法、#别名自动导入的触发与判定逻辑、生成的导入路径为何有时是#别名、有时是相对路径,以及如何通过测试源码验证这些行为。
从基线文件看场景:一个最小化的 Node16 自动导入用例
仓库中 autoImportPackageJsonImportsHashSlashNode16.baseline.md 是 fourslash 测试框架(BaselineAutoImportsCompletions)在某一输入上生成的基线快照,全文只有三块内容:
// === Auto Imports === // @FileName: /src/consumer.ts entit/**/import { entity } from "./domain/entities/entity"; entit这段基线揭示了两个事实:
- 输入文件中,光标位于
/src/consumer.ts,用户输入了entit前缀并期望自动导入补全; - 基线输出的导入语句为
import { entity } from "./domain/entities/entity",即选择了相对路径而非#别名。
要解释“为什么这里没有使用#别名导入”,必须回到生成该基线的测试输入。对应的测试定义位于 internal/fourslash/tests/autoImportPackageJsonImportsHashSlash_test.go:
func TestAutoImportPackageJsonImportsHashSlashNode16(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @Filename: /tsconfig.json { "compilerOptions": { "module": "node16" } } // @Filename: /package.json { "imports": { "#/*": "./src/*" } } // @Filename: /src/domain/entities/entity.ts export const entity = 1; // @Filename: /src/consumer.ts entit/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.BaselineAutoImportsCompletions(t, []string{""}) }测试场景还原如下:
| 文件 | 内容 | 作用 |
|---|---|---|
/tsconfig.json | module: "node16" | 启用 Node16 模块解析,打开resolvePackageJsonImports相关的默认行为 |
/package.json | "imports": { "#/*": "./src/*" } | 定义#子路径导入:#/domain/entities/entity等价于./src/domain/entities/entity |
/src/domain/entities/entity.ts | export const entity = 1; | 可导入的符号来源 |
/src/consumer.ts | entit/**/ | 触发自动导入的输入位置 |
而Nodenext变体(autoImportPackageJsonImportsHashSlash_test.go)则额外配置了rootDir: "./"与outDir: "build",且imports使用带条件的对象写法{ "types": "./src/*", "default": "./src/*" },用于验证在类型解析与运行时目标分离时的行为。
#/*通配符映射:Node.js 子路径导入的两种写法
Node.js 的package.jsonimports字段支持两种目标写法,两种写法都在仓库测试中出现:
写法一:直接字符串目标(Node16 基线用例)
{ "imports": { "#/*": "./src/*" } }此时#/domain/entities/entity会直接解析到./src/domain/entities/entity。Node16 模块解析器会把#之后的路径片段与*通配符做前缀匹配,并将捕获片段回填到目标中。
写法二:条件对象目标(Nodenext 用例)
{ "imports": { "#/*": { "types": "./src/*", "default": "./src/*" } } }条件对象允许为“类型解析”和“运行时解析”提供不同的映射目标。在语言服务侧,选择哪个分支由当前生效的 conditions 集合决定——这在 internal/ls/string_completions.go 中体现为conditions := module.GetConditions(compilerOptions, mode),随后getPatternFromFirstMatchingCondition会按 conditions 匹配顺序挑选第一个命中的模式(见 string_completions.go)。
从实现上看,当 key 以/结尾且 pattern 也以/结尾时,解析器会将其视为目录通配并拼接出pattern + "*"(string_completions.go),从而支持#/...下任意深度的子路径补全。
自动导入的完整链路:从补全触达到模块说明符生成
1. 补全触发与 imports 参与条件
在语言服务中,字符串补全逻辑位于 internal/ls/string_completions.go。自动导入补全的核心开关是resolvePackageJsonImports := compilerOptions.GetResolvePackageJsonImports()(string_completions.go),它对应 TypeScript 的resolvePackageJsonImports编译选项。只有该选项开启,且尚未遍历过当前包作用域(!seenPackageScope,见 string_completions.go)时,才会读取当前目录的package.json并进入imports查找分支:
importsLookup := func(directory string) { if resolvePackageJsonImports && !seenPackageScope { packageFile := tspath.CombinePaths(directory, "package.json") packageJsonInfo := program.GetPackageJsonInfo(packageFile) if packageJsonInfo != nil && packageJsonInfo.Exists() { seenPackageScope = true exportsOrImportsLookup(&packageJsonInfo.Contents.Imports, fragment, directory, false /*isExports*/, true /*isImports*/) } } }(string_completions.go)
这段逻辑的含义是:对于import {} from "#//"这类以#开头的模块说明符补全,语言服务会向上逐级查找祖先目录中的package.json,读取其imports表(packageJsonInfo.Contents.Imports),并基于#前缀键生成补全条目。
2.#前缀识别
当用户输入#起始的路径片段时,模块名解析会先切分路径组件:components[0]即#...形式的前缀。实现中通过strings.HasPrefix(packagePath, "#")识别到该前缀后,直接转交importsLookup(ancestor),不再走node_modules的exports查找(string_completions.go)。
3. 自动导入补全的#别名提议
值得强调的是:上述importsLookup链路服务于模块说明符(specifier)位置的补全。而自动导入(Auto Import)场景——即用户在某处直接输入符号名entit并期望语言服务补全import { entity } from ...——由 internal/ls/autoimport 包实现。对于这类补全,模块说明符的生成会综合包名、相对路径与子路径导入等多条候选,再依据用户偏好排序。
在仓库的同类测试中,可以看到自动导入在存在#映射时确实会提议#别名说明符,例如 autoImportPackageJsonImportsPattern_test.go:
// @module: node18 // @Filename: /package.json { "imports": { "#*": "./src/*" } } // @Filename: /src/something.ts export function something(name: string): any; // @Filename: /a.ts something/**/其断言为f.VerifyImportFixModuleSpecifiers(t, "", []string{"#something.js"}, nil),即自动导入建议的模块说明符是#something.js——由#*通配符把something回填进./src/*得到,且按node18模块模式补充了.js扩展名。
同样地,autoImportPackageJsonImportsPattern_ts_test.go 中映射为"#*": "./src/*.ts"时,建议的说明符是#something(不带扩展名),说明通配符目标的扩展名直接决定了说明符形态。
4. 为什么本基线选择了相对路径
那么,autoImportPackageJsonImportsHashSlashNode16的基线为何输出"./domain/entities/entity"而非"#/domain/entities/entity"?对照测试输入可见两个关键差异:
- 该测试的
imports映射是"#/*"(带/),而自动导入提议#别名依赖通配符回填规则与包内可见性判定; - 更重要的是,自动导入模块说明符的最终选择受用户偏好
ImportModuleSpecifierPreference约束。仓库中的 autoImportPackageJsonImportsPreference1_test.go 明确演示了这一机制:同样的#*映射与深层目录src/a/b/c/something.ts,在偏好设为"relative"(&lsutil.UserPreferences{ImportModuleSpecifierPreference: "relative"})时,自动导入生成的说明符是"./src/a/b/c/something"而非#别名。
可以推断,该 Node16 基线对应的默认补全环境下,语言服务权衡了模块说明符的稳定性(相对路径不受package.json映射变更影响)后选择了相对路径。这正是 Node16 模式下 TypeScript 自动导入的既定行为:#子路径导入会被纳入候选,但最终说明符以用户偏好与解析上下文为准。
模块说明符位置的#补全:通配符目录的逐级展开
除了自动导入,imports映射也直接驱动“手写导入路径”时的补全。测试 importCompletionsPackageJsonImportsPatternRootWildcard_test.go 展示了"#/*": "./src/*"下输入import {} from "#//"的补全结果:
// @module: nodenext // @Filename: /package.json { "imports": { "#/*": "./src/*" } } // @Filename: /src/something.ts export function something(name: string): any; // @Filename: /src/features/bar.ts export function bar(): any; // @Filename: /a.ts import {} from "#//*1*/";断言结果为两个补全项:something.js与features。这里features是目录条目(支持继续向下补全),something.js是最终可导入文件——这正是 string_completions.go 中“目录型通配符拼接pattern + "*"”后,结合路径片段枚举(getCompletionEntriesFromPathsOrExportsOrImports)的结果。它说明#/*映射在补全视角下等价于把./src/目录树“虚拟挂载”到#/之下。
结合仓库源码的验证方式:如何复现与扩展这条基线
该基线由 fourslash 测试框架的BaselineAutoImportsCompletions生成。若要在本地复现或修改场景,可直接编辑 internal/fourslash/tests/autoImportPackageJsonImportsHashSlash_test.go 中的测试输入,并通过go test运行对应测试(如TestAutoImportPackageJsonImportsHashSlashNode16)重新生成基线。相关实现与测试的探索路径:
- 补全/模块说明符生成主逻辑:internal/ls/string_completions.go
- 自动导入包注册与偏好处理:internal/ls/autoimport/registry.go(其中
AutoImportEntrypointDirectorySearch偏好控制是否递归搜索目录,DeepImportPackageNames用于标记无exports映射的深层导入包) - 同类自动导入用例(
#*/#/*/ 条件对象 / 偏好):internal/fourslash/tests/gen/autoImportPackageJsonImportsPattern_test.go、autoImportPackageJsonImportsPattern_ts_test.go、autoImportPackageJsonImportsPreference1_test.go - 路径级
#补全:importCompletionsPackageJsonImportsPatternRootWildcard_test.go(位于 internal/fourslash/tests/gen 目录)
这些生成测试文件(gen/目录下)由转换脚本自动生成,文件头部注明“Code generated by convertFourslash; DO NOT EDIT”,修改原始用例需遵循其标注的重新生成流程(如npm run makemanual ...)。
小结:一张表看懂本场景的行为矩阵
| 输入配置 | 触发方式 | 补全/导入结果 | 依据 |
|---|---|---|---|
"#/*": "./src/*",module: node16,输入entit | 自动导入补全 | import { entity } from "./domain/entities/entity"(相对路径) | Node16 基线、测试源码 |
"#*": "./src/*",module: node18,输入something | 自动导入补全 | import { something } from "#something.js"(#别名) | autoImportPackageJsonImportsPattern_test.go |
"#*": "./src/*.ts",module: node18 | 自动导入补全 | import { something } from "#something"(无扩展名) | autoImportPackageJsonImportsPattern_ts_test.go |
"#*": "./src/*.ts",偏好ImportModuleSpecifierPreference: "relative" | 自动导入补全 | import { something } from "./src/a/b/c/something"(相对路径优先) | autoImportPackageJsonImportsPreference1_test.go |
"#/*": "./src/*",输入#// | 模块说明符补全 | 补全something.js与目录features | importCompletionsPackageJsonImportsPatternRootWildcard_test.go |
综上,autoImportPackageJsonImportsHashSlashNode16这条基线虽然只有寥寥数行,背后却串联了 typescript-go 语言服务中“编译选项门控 →package.jsonimports 读取 → 通配符回填 → 用户偏好排序 → 说明符生成”的完整链路。理解它,就理解了现代 Node 模块体系下自动导入与#子路径导入的协作方式。
- 编译器
- 编程语言
- 开发工具
【免费下载链接】typescript-go
Staging repo for development of native port of TypeScript
相关推荐
TypeScript 自动导入中的 importModuleSpecifierPreference:相对路径模块说明符偏好机制全解
TypeScript 自动导入中的 importModuleSpecifierPreference:相对路径模块说明符偏好机制全解 自动导入(Auto Impo
编程语言编译器开发工具react-use 的 useUnmountPromise:组件卸载后永不解析的 Promise 生命周期 Hook 实战指南
react use 的 useUnmountPromise:组件卸载后永不解析的 Promise 生命周期 Hook 实战指南 useUnmountPromis
编译器编程语言开发工具wtfjs中的模块说明符:JavaScript导入路径陷阱
wtfjs中的模块说明符:JavaScript导入路径陷阱 JavaScript的模块系统看似简单,实则暗藏玄机。本文将深入解析wtfjs项目中模块说明符的使用
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考