尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
深入 Freelens 扩展契约测试桩:`@freelensapp/fixture-extension` 如何让“静默破坏“无处遁形
云原生开发工具运维【免费下载链接】freelensFree IDE for Kubernetes项目地址https://gitcode.com/gh_mirrors/fr/freelens点击查看免费下载导读FreelensKubernetes 免费 IDE通过freelensapp/extensions向第三方暴露扩展契约而这份契约一旦出现宿主未发布单例命名空间成员只有值没有类型生命周期中没有可注册点等破坏普通代码检查往往浑然不觉。本文以仓库内私有的packages/fixture-extension/README.md为主线剖析freelensapp/fixture-extension这一测试桩扩展的三层验证设计——类型层、单元层、集成层并结合源码展示它如何以最接近真实扩展的方式构建、加载与断言让读者既理解其设计动机也能直接复用其 tsconfig 划分、环境探针与 bundler 外部映射等手法。它是什么一个刻意最小、只为验证契约而生的私有扩展freelensapp/fixture-extension是一个私有的、刻意最小化的 Freelens 扩展唯一使命是测试 Freelens 自身的扩展契约extension contract。它的package.json中private: true包元数据 注明其作用为 in-repo test fixture exercising the extension contract且明确永不发布never published——整个仓库中只有集成测试会安装它。它的存在源于一个真实痛点仓库内没有任何其他代码端到端地走一遍扩展契约于是一整类破坏曾长期静默存活宿主没有把自己的单例singleton发布到全局命名空间成员有值没类型exists as a value but not as a type生命周期中没有一个点让扩展能注册任何东西。这类问题普通单元测试看不出来但一个真实构建、真实加载的扩展会立刻失败。fixture 就是用来让这些失败在 CI 里当场暴露的。关键前提它不是模板原文档特意强调This is not a template。若你需要拷贝起点官方角色由独立的公开示例仓库 freelens-example-extension 承担——它证明已发布的freelensapp/extensions包在真实创作条件下可被外部使用。而本仓库的 fixture 刻意保持契约允许范围内最小它导出真实扩展绝不会导出的东西如resolvedMobxObservable、resolvedReactUseState并会随任何有静默破坏风险的变化持续改变形态。克隆它只会得到一个更差的起点还会背上对本仓库测试设施的依赖。构建方式像真实扩展一样构建结论才可信fixture 的构建刻意模拟真实扩展见vite.config.mjsfreelensapp/extensions是它唯一的运行时依赖所有宿主提供的模块host-provided modules都由打包器映射到globalThis.FreelensExtensionApi。原因在构建注释中写得很清楚打包进一份 React 意味着渲染进程里有两份 React任何 hook 都会抛invalid hook call打包进一份 mobx 不会抛错——两份 mobx 通过共享的全局状态继续互操作宿主永远不会对扩展创建的 observable 做出反应。两者都是纯运行时失败只有把 bundle 真正构建并加载才会显形。所以 fixture 必须按真实扩展的打包方式构建用任何其他方式构建的 fixture 都证明不了扩展实际如何被加载。三层验证架构Types / Unit / Integrationfixture 的验证被组织成三层各司其职、互为补充层位置能捕获什么Typespnpm --filter freelensapp/fixture-extension type:check由其build脚本运行从已发布表面消失的 re-export包括有值但无法以类型命名的成员某个 tsconfig 放行另一运行时环境的 API 混入本环境代码Unitpackages/core/src/extensions/__tests__/fixture-extension.test.tsxReact 与 mobx 的实例身份、注册器registrators、生命周期、Util.fetch到达宿主的 DIIntegrationfreelens/integration/__tests__/extensions.tests.ts针对pnpm build产生的dist/打包应用本就需要它打包后的应用就地安装本目录、接受其engines.freelens并用宿主发布的 React 渲染其带 hooks 的状态栏项其中build脚本package.json为build: pnpm type:check vite build vite build --mode main, type:check: pnpm type:check:sources pnpm type:check:tooling pnpm type:check:environments, type:check:sources: tsc -p src/main tsc -p src/renderer tsc -p src/common, type:check:tooling: tsc -p tsconfig.json, type:check:environments: tsc -p environment-tests/tsconfig.main.json tsc -p environment-tests/tsconfig.renderer.json tsc -p environment-tests类型层如何接线一套 tsconfig三个运行时环境fixture 的源码布局严格遵循docs/extensions/migrating-from-v1.md中为扩展规定的Source layout: one tsconfig per runtime environment而这个包正是该布局的证明场目录运行环境tsconfig.json要点src/main/主进程Node 与 Electronlib: [ES2024]、types: [node]并include了src/common/src/renderer/浏览器页面lib含DOM/DOM.Iterable、types: []并include了src/common/src/common/两者都运行分别打进两个入口lib: [ES2024, WebWorker]、types: []具体配置见src/main/tsconfig.json、src/renderer/tsconfig.json、src/common/tsconfig.json它们都只继承tsconfig.base.json。后者承载了迁移指南给扩展消费方规定的编译器选项底线target: ES2024、module: ESNext、moduleResolution: Bundler、strict、noImplicitAny、noUnusedLocals、isolatedModules、skipLibCheck、noEmit以及关键的paths: { freelensapp/extensions: [../extensions/dist/extension-api.d.ts] }common 代码被检查三次type:check依次编译三个程序于是src/common/的代码被检查三次被main 程序编译——它没有 DOM被renderer 程序编译——它没有 Node被common 自己的配置编译——这正是编辑器打开src/common/下文件时所用的配置。与此同时type:check:tooling用根目录tsconfig.json编译vite.config.mjs该配置开了allowJs/checkJs且带 Node 类型于是构建配置本身也像源码一样经受 JSDoc 类型检查。环境探针用ts-expect-error反转检查能编译的配置不等于能隔离环境的配置。为此environment-tests/下的探针文件与源码用同一设置、同一include一起编译每一行必须编译失败的代码都带ts-expect-error——这反转了检查方向一旦某个配置开始放行该行就会因未使用的指令unused directive而失败。配置编译内容采用谁的设置environment-tests/tsconfig.main.jsondom-apis.ts、worker-apis.ts必须失败shared-apis.ts必须通过src/main/environment-tests/tsconfig.renderer.jsonnode-apis.ts必须失败shared-apis.ts必须通过src/renderer/environment-tests/tsconfig.jsonnode-apis.ts、dom-apis.ts必须失败shared-apis.ts必须通过src/common/探针内容同样值得研读node-apis.tsnode:fs的具名与副作用导入、Buffer、process.env.HOME全部ts-expect-error——浏览器页面没有这些dom-apis.tsdocument.title、window.location.origin——主进程没有 DOMworker-apis.tsself、postMessage——这正是 common 代码也要用 main 配置再编译一次的原因worker 有、Node 没有common 的WebWorkerlib 接受它们只有 main 程序拒绝shared-apis.tsglobalThis.crypto.randomUUID、TextEncoder/TextDecoder、URL/URLSearchParams、AbortController、structuredClone、定时器与queueMicrotask——两个运行时都有的全局三个配置都必须放行。注意定时器句柄被刻意保持不透明浏览器里是 numberNode 里是 object。探针必须与源码一起编译而非单独编译原因在迁移指南里也有印证声明文件里的一行/// reference typesnode /或/// reference libdom /会把整套类型拉进任何通过 import 触及它的程序而这类声明只有源码导入对应包时才会进入程序——单独编译探针会漏掉这条路径。与 Biome 规则联动仓库根biome.jsonc对src/renderer/与src/common/两个目录应用了 Biome 的noNodejsModules规则——在 renderer 或 common 里import一个 Node 内置模块会同时是 lint 错误。这与迁移指南对任何扩展的建议一致类型层与 lint 层互相补位。唯一的路径映射整个包的灵魂fixture 的 tsconfig 刻意保持独立只继承自己的tsconfig.base.json不继承仓库 tsconfig除了那一个路径映射外不声明任何 workspace 路径映射——因为扩展作者两者都没有。那一个映射就是paths: { freelensapp/extensions: [../extensions/dist/extension-api.d.ts] }它把freelensapp/extensions指到构建产物../extensions/dist/extension-api.d.ts。若交给 pnpm 的 workspace 链接specifier 会解析到packages/extensions/src/extension-api.ts——TypeScript 源码一种真实作者永远见不到的形状于是消费方实际安装的那份单一 bundle 声明将无人检查。一个从 bundle 中消失的 re-export会让这里的tsc直接失败。该声明由pnpm --filter freelensapp/extensions build产生fixture 的build通过 turbo 依赖它。同理fixture 被排除在仓库根的tsconfig.typecheck.json之外那里的paths会把freelensapp/extensions映射回 workspace 源码按源码检查会毁掉 fixture 存在的唯一意义且声明只在构建后才存在而类型检查工作流刻意不跑构建——所以 fixture 自己的tsc归属其buildpnpm build与单元测试工作流都能触达它。运行时层bundler 如何把宿主模块映射到全局vite.config.mjs是整个 fixture 的运行时关键。它用一个enforce: pre的插件freelens-host-provided-modules抢在 Vite 自己的 resolver 之前把以下 specifier 替换为从globalThis.FreelensExtensionApi读回同一值的虚拟模块const hostProvidedModules { freelensapp/extensions: const api ${HOST_GLOBAL}; export const Common api.Common; export const Main api.Main; export const Renderer api.Renderer; , react: const React ${HOST_GLOBAL}.React; export default React; export const { Component, useCallback, useMemo, useState } React; , react/jsx-runtime: const jsxRuntime ${HOST_GLOBAL}.ReactJsxRuntime; export const { Fragment, jsx, jsxs } jsxRuntime; , mobx: const mobx ${HOST_GLOBAL}.Mobx; export const { computed, observable, runInAction } mobx; , };这正是已发布的freelensapp/extensions运行时 shim 所做的事——packages/extensions/src/runtime-shim.ts被原样转译进dist/extension-api.js它只 re-export 宿主启动时赋值的globalThis.FreelensExtensionApi因此产物没有任何运行时依赖其余几个则是宿主在全局对象上同时 re-export 的单例。这份具名导出清单是手工维护的且是故意的fixture 里新增一个该文件未覆盖的 import构建会以 is not exported by 失败而不是静默打包第二份副本。构建产出两个入口build两次运行renderer默认src/renderer/index.tsx→dist/renderer.js面向浏览器页面emptyOutDir: trueexternal: []main--mode mainsrc/main/index.ts→dist/main.js面向 NodeemptyOutDir: falseexternal: [/^node:/]——Node 在运行时自行解析自己的 builtins且只有 main 允许导入它们src/renderer/tsconfig.json没有 Node 类型那里的导入会在类型检查阶段先失败。两个入口无法共用设置且每个 bundle 各自携带一份src/common/的拷贝而非共享一个需要另一进程加载的 chunk。target: esnext、minify: false、formats: [es]——不压缩是为了让这份 bundle 可供人审阅也供单元测试原样加载。单元层在 core 的测试台里断言身份而非形状单元层位于packages/core而不是 fixture 包内有两个原因它的测试台需要 core 的getDiForUnitTesting且若 core 对这个包建立 workspace 依赖会在 turbo 任务图里闭合一个环extensions#build→ core → 本包 →extensions#build。packages/core/src/extensions/__tests__/fixture-extension.test.tsx用file URL 而非静态 import加载构建产物dist/renderer.jsbundle 在求值时如果全局尚未安装就会抛错这正是契约本身必须在测试内部发生且 specifier 不能被 TypeScript 解析否则会去类型检查一个构建产物。若 bundle 未构建测试会直接报错并提示先运行pnpm --filter freelensapp/fixture-extension build。它断言的六件事正好一一对应 fixture 设计的六个静默破坏点宿主的单例按身份共享而非按形状测试先断言globalThis.FreelensExtensionApi上发布的是宿主自己的实例React、ReactDom、ReactJsxRuntime、Mobx、MobxReact、MonacoEditor全部用toBe与测试文件自身的 import 比对并断言除这六个外加Common/Renderer外没有任何单例——宿主的 DI 库刻意保持内部expect(published).not.toHaveProperty(OgreToolsInjectable)再通过 bundle 导出的resolvedMobxObservable与resolvedReactUseState比对observable与React.useState本身。对 mobx 而言这是关键断言两份 mobx 6 仍可通过共享的全局状态互操作反应照常触发行为断言全绿的同时扩展却带着一份副本——只有身份比对能抓住它。生命周期与注册器new fixture.default(installedFixture)构造扩展、hostExtension.register()注册后宿主statusBarItems计算中必须出现来自status-bar-items注册器的条目断言item.origin包含扩展名。观察用autorun包裹——未观察的 mobx computed 每次.get()都会重算不追踪任何东西也能通过。mobx 反应fixture.setStatusBarItemVisible(false)后条目消失、true后重现证明宿主通过宿主自己的 mobx对扩展创建的 observable 做出反应。Renderer.Util.fetch到达宿主 DI扩展onActivate里Renderer.Util.fetch(FIXTURE_PROBE_URL)测试用browserFetchInjectable的 mock 断言fetchMock收到该 URL且activationRecord记录的appVersion正是测试覆盖的构建版本1.2.3——证明 API 命名空间能触达宿主的 DI 容器与Common.Util。detailsFor/menuItemFor存在且模型保持窄化字面量extension.kubeObjectDetailItems等于{ kind: FixtureExample, apiVersions: [fixture.freelens.app/v1alpha1], components: { Details: fixture.FixtureExampleDetails } }菜单同理构造FixtureExample实例后kind/apiVersion保持窄化值——declare不发射任何代码换成普通字段则会在基类构造赋值后被覆盖。customResources注册读取类的crd注入extensionCustomResourceInjectionToken得到{ group: fixture.freelens.app, plural: fixtureexamples, extensionName }。测试还渲染了带 hooks 的FixtureStatusBarItemrenderfireEvent.click断言fixture:0→fixture:1这正是两份 React 会让每个 hook 当场抛invalid hook call的验证点。集成层打包后的应用真正安装它freelens/integration/__tests__/extensions.tests.ts通过 Playwright 驱动真实打包应用前置校验dist/main.js与dist/renderer.js存在否则提示先跑pnpm build或pnpm build:fixture-extension——测试自身不构建因为 CI 里 turbo 等 devDependencies 已不在用应用菜单导航到扩展页把fixture 包目录本身填入安装框Name, URL, or path to a package or directory点击 Install 并确认——目录被就地注册应用加载的正是pnpm build写入dist/的内容断言扩展表格行状态列显示 Enabled若engines.freelens门槛拒绝该列会显示 Incompatible并等待[data-testidfixture-status-bar-item]出现——它只经由 fixture 的statusBarItems注册、且用宿主 React 渲染 hooks出现即证明 renderer 入口在宿主 React 上成功运行。fixture 里到底有什么六处静默破坏点的完整清单renderer 入口src/renderer/index.tsx→dist/renderer.js带 hooks 的组件FixtureStatusBarItemuseState/useMemo/useCallback——两份 React 时每次渲染都抛invalid hook call扩展创建、宿主必须反应的 observablestatusBarItemIsVisible observable.box(true)宿主status-bar-itemscomputed 读取它——两份 mobx 时宿主 computed 不再追踪这个 box、永不失效不抛错只靠身份断言抓声明式注册statusBarItems——只有注册器与extension.register()生命周期都工作才到达宿主Renderer.Util.fetch(FIXTURE_PROBE_URL)——只通过宿主 DI 容器解析kubeObjectDetailItems与kubeObjectMenuItems用Renderer.K8sApi.detailsFor/menuItemFor对模型类src/renderer/fixture-example.ts注册——helper 在运行时按声明存在才构造得出来模型类用declare kind/declare apiVersion把字段窄化为字面量且declare不发射代码、必须让宿主赋的值原样保留customResources注册同一模型类——宿主注册器要能从 bundle 读取类的crd才行。FixtureExample的静态crd声明了apiVersions、plural、singularapiBase为/apis/fixture.freelens.app/v1alpha1/fixtureexamples。bundle 还把resolvedMobxObservable、resolvedReactUseState、FixtureExample等 re-export 出来供测试台比对——真实扩展不会这么干但这就是契约。main 入口src/main/index.ts→dist/main.js主进程侧的契约骨架一个Main.LensExtensiononActivate中FixtureIpc.createInstance(this).handle(HOST_INFO_CHANNEL, ...)注册Main.Ipchandler返回os.platform()、createRequestId()、getProbeHost()并导入 Node builtinnode:os——证明Main.LensExtension、Main.Ipc与 Node builtin 都能在src/main/tsconfig.json有types/node、无 DOM下编译且 bundle 以 builtins 保持 external 的方式构建成功。两入口共享src/common/host-info.ts只使用两个运行时都有的全局FIXTURE_PROBE_URL https://fixture.invalid/contract-probe从不会被真实请求宿主 DI 直接服务它、HOST_INFO_CHANNEL host-info、globalThis.crypto.randomUUID()绝不node:crypto、WHATWGURL。纯类型层文件不进入任何 bundlesrc/common/contract-types.ts无运行时代码在真实签名中指名Common/Main/Renderer的类型。它专门捕获 #2365 那一类问题——命名空间以值 re-export 但不可作为类型命名的符号在签名里根本写不出来失败是编译错误而非别人仓库里的运行时惊吓它还覆盖 main 入口没用到的Main部分、以及任何运行时断言都触不到的类型成员。文件里甚至用HoldsT extends true与ts-expect-error组合断言页面组件只收params菜单项只收object与toolbar注册组件不得要求extension实例等契约细则。src/common/v1-renames.ts为docs/extensions/migrating-from-v1.md中 v1→v2 rename table 的每一行 Renamed or moved命名 v2 侧路径并标注 v1 路径如表中的getDetailsUrl/showDetails→Renderer.Navigation、getActiveTheme()→Renderer.Theme.activeTheme.get、ResourceStack→createResourceStack、isExtensionNameInstallRegex→RegExp.test/extensionNameInstallCaptures。表里加一行这里就加一行某行指引的替代品从构建声明中不可达构建即失败而不是等到移植时才爆。src/renderer/registration-pairings.tsdetailsFor/menuItemFor必须拒绝的错误配对——为Pod写的组件注册给Deployment函数式与类组件两种形态、v1alpha1组件注册给v1alpha2类及反向两种 spec 只差可选字段、彼此可赋值唯有类声明的字面量apiVersion能区分。每处都带ts-expect-error声明一旦丢失类与组件的绑定就会以未使用指令失败同文件的正向配对与narrowedModelApis则证明窄化模型仍适配KubeApi/KubeObjectStore类型。给扩展作者与 Freelens 维护者的启示对扩展作者而言fixture 是迁移指南布局src/main/src/renderer/src/common三套 tsconfig 环境探针 BiomenoNodejsModules的可运行范本复制它的目录结构与tsconfig.base.json把paths指向你实际消费的声明产物就能在本地获得两环境隔离 契约可达性双重类型保障。对Freelens 维护者而言fixture 是一张活的契约红绿灯新增或改动扩展 API 时先看type:check是否变红再看单元与集成层是否复绿而按身份而非形状共享单例mobx 反应必须从autorun内观察探针必须与源码同编译这三条测试纪律正是它区别于普通toHaveProperty断言列表的价值所在——它测的是契约是否真的成立而非表面属性是否齐全。赞分享云原生开发工具运维【免费下载链接】freelensFree IDE for Kubernetes项目地址https://gitcode.com/gh_mirrors/fr/freelens点击查看免费下载相关推荐使用 Error Prone JUnit3TestNotRun 检查器让被 JUnit 3 静默忽略的测试方法无所遁形使用 Error Prone JUnit3TestNotRun 检查器让被 JUnit 3 静默忽略的测试方法无所遁形 JUnit 3 依靠方法名以 tes静态分析代码质量开发工具Memgraph开源图数据库入门10分钟快速上手实时数据分析Memgraph开源图数据库入门10分钟快速上手实时数据分析 Memgraph 是一款专为动态分析环境优化的开源图数据库兼容Neo4j的Cypher查询语言数据库图数据库向量数据库后端Json.NET JSON Schema校验实战JsonValidatingReader如何让非法JSON无处遁形Json.NET JSON Schema校验实战JsonValidatingReader如何让非法JSON无处遁形 在 .NET 生态中 Json.NET序列化后端上一篇BlobFuse2 vs BlobFuse v1为什么2026年前必须完成迁移关键差异对比下一篇gdsfactory PDK实战如何快速集成42工艺设计套件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

ant-design-blazor TreeSelect 弹出位置(placement)完全指南:手动指定下拉弹出方向与底层实现解析

ant-design-blazor TreeSelect 弹出位置(placement)完全指南:手动指定下拉弹出方向与底层实现解析

前端UI组件设计系统 【免费下载链接】ant-design-blazor 基于 Ant Design 与 Blazor 的前端组件库。让开发者解放生产力,实现更大价值。 项目地址: https://gitcode.com/ant-design-blazor/ant-design-blazor 点击查看 免费下载 placement 是 ant-desig…

📅 2026/10/12 3:22:35
使用 Jaeger Go 客户端(jaeger-client-go)为 Go 服务接入 OpenTracing 分布式追踪

使用 Jaeger Go 客户端(jaeger-client-go)为 Go 服务接入 OpenTracing 分布式追踪

云原生可观测性容器编排运维 【免费下载链接】scope Monitoring, visualisation & management for Docker & Kubernetes 项目地址: https://gitcode.com/gh_mirrors/sc/scope 点击查看 免费下载 jaeger-client-go 是 Uber 提供的 Jaeger 官方 Go 探针库&am…

📅 2026/10/12 3:22:35
Infosec_Reference 之 ICS/SCADA 安全资源指南:从协议原理到攻防工具链

Infosec_Reference 之 ICS/SCADA 安全资源指南:从协议原理到攻防工具链

网络安全教程 【免费下载链接】Infosec_Reference An Information Security Reference That Doesnt Suck; https://rmusser.net/git/admin-2/Infosec_Reference for non-MS Git hosted version. 项目地址: https://gitcode.com/gh_mirrors/in/Infosec_Reference 点击…

📅 2026/10/12 3:22:35
MORE NEWS

更多资讯

📰

多隐层网络梯度消失与Xavier、He初始化的数理推导

多隐层网络的训练困难,十次里有九次出在梯度在层与层之间传递时出了问题。最近帮一位朋友排查一个四层全连接网络,结构不复杂,数据也正常,但损失就是卡在某个值附近下不去。我当时第一反应不是调学习率,而是让他把每一…

📰

mahjong-helper实战指南:牌效分析、防守判断与记牌技巧详解

简介:mahjong-helper是一款面向雀魂、天凤玩家的日本麻将实时辅助工具,核心解决对局中的牌效判断、防守安全度与记牌需求。它能自动分析手牌,综合进张与打点给出推荐舍牌,并在有人立直或多副露时标注各牌危险度,同时记…

📰

出口IP失效排查与可用性提升实战:健康检查、重试与熔断策略

看标题进来的朋友,应该都有同一种体验:明明昨天还好好的网络出口 IP,今天突然大面积超时;又或者某个 IP 对 A 站点通得飞快,一换到 B 站点就立刻被拒。这类问题在多点拨测、自动化数据采集、接口灰度验证等场景里太常见…

📰

数字人分身源码实战:音频驱动的批量口播视频生成与避坑指南

简介:数字人分身系统源码是一套面向短视频创作者、自媒体运营者和商业内容团队的视频制作工具,通过克隆声音、动作与表情生成虚拟分身,帮助不便出镜或易忘词的用户快速产出高质量口播视频。压缩包共2005个文件,以js、md、css、jso…

📰

Kubernetes节点NotReady根因:CNI配置未初始化详解

1. 问题现场还原:K8S节点卡在 NotReady,CNI 配置未初始化的典型症状刚接手一个某高校实验室搭建的轻量级教学集群,三台物理机部署了 Kubernetes v1.13.12(这个版本虽已归档,但在教学环境和老旧硬件上仍有大量存量使用&…

📰

短视频配音工具哪个好用

说明本文基于公开产品体验与多方使用反馈整理,不含任何商业合作,仅作为选型参考。文中产品均按公开信息描述,具体功能以各平台官方页面为准。结论先看短视频配音工具没有哪个绝对最好,选型的核心就是场景匹配。已经在用剪映做视频…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬