news 2026/9/18 3:22:58

Jest 单体仓库开发实战:从环境搭建、测试体系到源码架构的完整工作手册

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Jest 单体仓库开发实战:从环境搭建、测试体系到源码架构的完整工作手册

Jest 单体仓库开发实战:从环境搭建、测试体系到源码架构的完整工作手册

【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest

在 Jest 官方仓库中,CLAUDE.md 是写给 AI 编码代理的"入口导航",它通过@.github/copilot-instructions.md导入方式指向一份 17KB 的详细工作指令文档 .github/copilot-instructions.md,涵盖环境搭建、构建体系、测试命令、Lint 硬规则、Mock 类型化模式、代码规范以及"如何定位每个功能在源码中的位置"等全部内容。读完本篇,你将掌握在 Jest 仓库中完成一次完整开发闭环(安装依赖 → 构建 → 跑单测/e2e → 类型检查 → Lint → 提交前校验清单)所需的全部实操细节,并理解其 55 个包组成的单体仓库内部各组件的协作关系。

CLAUDE.md 的作用:一个"导入式"的代理指令文件

仓库根目录的 CLAUDE.md 本体只有两行:

# CLAUDE.md @.github/copilot-instructions.md

这是 Claude Code 类工具支持的@file导入语法:代理加载CLAUDE.md时会自动展开被引用的文件内容。真正承载全部技术内容的就是 .github/copilot-instructions.md(标题为 "Jest Repository — Coding Agent Instructions")。值得注意的是,仓库并非只有一份这类文档——根文档在 "When in doubt" 一节中列出,expectjest-circusjest-configjest-environment-nodejest-fake-timersjest-haste-mapjest-mockjest-reportersjest-resolvejest-runtimejest-snapshotjest-source-mapjest-transformjest-worker这 14 个关键包各自还维护了一份包级CLAUDE.md(例如 packages/jest-runtime/CLAUDE.md),用于沉淀包内特有的坑与约定。

这份文档的定位是"可被机器执行的操作手册",因此它的每一节都对应仓库中真实存在的脚本、配置或 CI 行为,本文按原文骨架逐节展开,并结合源码做纵深验证。

仓库形态与环境前提

文档开篇给出的仓库画像可以直接从仓库文件得到印证:

  • 大型单体仓库:55 个包、200 余个 e2e fixture,位于 packages/ 与 e2e/ 目录下;
  • 工具链:Lerna-lite(lerna.json 中version: 30.4.2,根 package.json 依赖@lerna-lite/cli@lerna-lite/publish)+ Yarn 4(Berry,node-moduleslinker,不是 PnP);根 package.json 中"packageManager": "yarn@4.18.0"
  • 语言:全 TypeScript,每个包用 Webpack 单独编译;
  • Node 引擎^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0,与根 package.json 的engines字段完全一致,并被 yarn.config.cjs 的约束规则同步强制到所有公开发布的工作区上。

环境搭建:install 之后必须先 build:js

文档给出的 Setup 步骤只有三条命令,但背后藏着这个仓库最容易踩的坑——源码与构建产物的双轨加载机制

corepack enable yarn install # 约 45 秒,需要 Python(node-gyp 用) yarn build:js # 约 5 秒,必须执行

yarn build:js之所以是强制步骤,源于文档明确解释的双轨机制:

  1. 单元测试从源码跑:各包__tests__/里的import {x} from '../'解析到该包的src/,由babel-jest即时转译(jest.config.mjs 中transform正是{'\\.[jt]sx?$': require.resolve('babel-jest')});
  2. e2e 测试跑构建产物:e2e fixture 直接调用packages/jest-cli/bin/jest.js,该入口从build/加载所有包;同时跨 workspace 包的 import 也经由main字段指向build/

因此"每次 checkout 后、每次修改了其他包或 e2e fixture 会消费的包之后",都必须重新yarn build:js。完整的yarn build(=build:js && build:ts && bundle:ts)耗时 3–5 分钟,涉及 API Extractor 类型声明产物,仅在修改类型声明或 API 输出、以及运行yarn typecheck:tests之前才需要(后者从build/*.d.ts解析跨包类型,缺少构建产物会报"幻影错误")。

迭代与清理命令:yarn watch(Webpack 热编译)、yarn watch:ts(声明文件 watch,即yarn build:ts --watch)、yarn build-clean(rimraf 掉所有packages/*/builddisttsconfig.tsbuildinfo)、yarn clean-all(再清 e2e 临时产物与 node_modules)。这些脚本均定义在根 package.json 的scripts字段中,可直接查证。

测试体系:命令、配置与 CI 行为

常用测试命令

文档列出的命令全部来自根 package.json 的 scripts,可原样复制使用:

yarn jest <path> # 跑指定文件或目录 yarn jest-runtime-vm-modules # 以 --experimental-vm-modules 跑 jest-runtime 的 ESM 测试 yarn workspace <name> test # 只跑单个包 yarn jest-coverage # 带覆盖率 yarn jest-jasmine-ci # CI 模式 + jasmine2 runner yarn test-leak # 对 jest-mock/jest-diff/pretty-format 做 detectLeaks yarn test-types # tstyche 类型级测试(__typetests__/ 目录) yarn test-ts # TypeScript 配置集成测试(独立配置) yarn test-ci-partial:parallel --max-workers <N> --shard=<M>/<N> # CI 分片

几个值得展开的实现细节:

  • yarn jest并非调用 npm 上的 jest,而是node ./packages/jest-cli/bin/jest.js——用自己仓库的 CLI 测自己
  • yarn jest-runtime-vm-modules的真实定义是NODE_OPTIONS="--experimental-vm-modules --no-warnings" yarn jest packages/jest-runtime,对应 package.json 中的 script;
  • yarn test-leak实际为yarn jest -i --detectLeaks --color jest-mock jest-diff jest-source-map pretty-format

三份 Jest 配置与默认运行参数

配置用途
jest.config.mjs主配置
jest.config.ci.mjsCI 专用:叠加github-actionsjest-junitjest-silent-reportersummary四组 reporter,覆盖率输出 json
jest.config.ts.mjstest-ts集成测试专用

从 jest.config.mjs 可以读出文档所述默认值的出处:testTimeout: 70_000(默认超时 70 秒)、projects: ['<rootDir>', '<rootDir>/examples/*/'](examples 目录也是测试工程)、snapshotSerializers使用jest-serializer-ansi-escapes(这正是后文"快照含 ANSI 序列"陷阱的根源)。默认 runner 是jest-circus,设置环境变量JEST_JASMINE=1可切换到兼容用的jest-jasmine2(对应yarn jest-jasminescript)。新测试文件一律使用.ts后缀(仓库中仍有少量遗留.js)。

关于类型检查的一个硬约束:每个被yarn typecheck:tests覆盖的包内__tests__/目录都有各自的tsconfig.jsonextends根 tsconfig.test.json(其中noEmit: truetypes: ["@jest/test-globals"])。当测试用到Console/Stats/__dirname等 Node 全局时,需要向该目录 tsconfig 的types数组中追加"node"yarn typecheck:tests的 glob 列表(tsc -b packages/{...}/**/__tests__)写在根 package.json 中,新增包必须把该包追加进这个 glob——这一条在 CI 中是门控项(必须 exit 0)。

类型测试的"归属规则":matcher 的类型测试放 jest-types

文档特别强调了一条容易被忽略的约定:

  • expectmatcher 的类型测试属于 packages/jest-types/typetests/expect/(该目录下已有toHaveBeenCalledWith.test.tstoHaveBeenLastCalledWith.test.ts等文件),不放packages/expect/__typetests__/
  • 原因:jest-types测的是用户视角的公开@jest/globals面(用户实际 import 的类型),而packages/expect/__typetests__/只覆盖expect包内部关切(如MatcherFunctionJestExpect形状)。

e2e 测试的约束与手动运行

  • e2e 测试(e2e/tests/)禁止使用jest.mock/jest.fn——ESLint 规则强制执行,原因是 e2e 进程本身是独立启动的 jest 实例,mock 无法按单测语义工作;正确做法是编写 fixture 文件;
  • 部分 e2e 测试依赖 Mercurial,需要brew install hg
  • 手动跑单个 e2e fixture:
cd e2e/<test-directory> node ../../packages/jest-cli/bin/jest.js --no-cache

Docblock pragma:单文件级环境覆盖

文档说明测试文件头部的 docblock 注释可覆盖测试环境:

  • @jest-environment <name>:覆盖该文件的 test environment;
  • @jest-environment-options {"key": value}:合并进testEnvironmentOptions

从源码可确认其实现位置:packages/jest-runner/src/runTest.ts 中,docblock.parse(docblock.extract(testSource))解析 pragma,读取jest-environment字段(且报错提示"只允许定义单一环境"),随后在 L151-L155 解析jest-environment-options的 JSON 字符串并合并——两个 pragma 都只对该文件生效

CI 行为解读:为什么"本地红、CI 绿"

CI 矩阵定义在 .github/workflows/test.yml:Ubuntu/macOS/Windows × Node 18/20/22/24/25/26,测试步骤通过nick-fields/retryaction 包裹,配置为timeout_minutes: 10, max_attempts: 3, retry_on: error(见 test.yml L41-L46),并以--shard=M/N切分。文档由此给出一条排障经验:若某测试本地稳定失败而 CI 常绿,怀疑是被重试掩盖的 flaky

另一个反直觉点在覆盖率:codecov/patchcodecov/project在四个Node LTS on Ubuntu with coverage (N/4)分片全部上传前,数字没有意义;且两者本身是参考性的(require_ci_to_pass: false, target: auto)。覆盖率的统计口径也有限制——只统计运行套件所在进程内被执行的代码,因此"只能经 e2e fixture 触达"的行永远显示为未覆盖。

四条值得背下来的测试陷阱

文档 "Test gotchas worth memorizing" 一节给出四条高频坑,均已在仓库中可验证:

  1. ANSI 颜色快照:很多快照包含 chalk 渲染的 ANSI 转义序列(jest.config.mjs 注册了jest-serializer-ansi-escapes)。更新快照必须FORCE_COLOR=1 yarn jest <path> -u,否则颜色序列被剥掉、生成错误快照;
  2. Windows CI 上的路径断言:期望值由path.join/path.dirname/path.basename构造时,断言侧也要用path.join构造,硬编码'/path/to/x'这类 POSIX 字符串在 Windows 上必挂;
  3. 扫描 globalThis 时防"抛异常的 getter":遍历Object.keys(scope)再读scope[key],会在用户安装了 throwing getter 时崩溃;应以'key' in scopehas陷阱而非get)作门控;
  4. ESM 条件测试辅助函数@jest/test-utils提供三个 ESM 条件用例辅助(源码见 packages/test-utils/src/ConditionalTest.ts):
    • testWithVmEsm:Node 18+ 需--experimental-vm-modules
    • testWithLinkedSyntheticModule:Node 22.21+/24.8+,以linkRequests能力门控;
    • testWithSyncEsm:Node 24.9+,以hasAsyncGraph门控(与 packages/jest-runtime/src/internals/nodeCapabilities.ts 中supportsSyncEvaluate探测SourceTextModule.prototype.hasAsyncGraph的实现相呼应);
    • 特别注意:yarn jest packages/jest-runtime不包含ESM 套件,必须用yarn jest-runtime-vm-modules

Lint:硬规则与格式化

文档要求"每次编辑后立即 lint 变更文件":

yarn eslint --cache --fix <files> # 开发中 yarn lint # 推送前全量(eslint . --cache --ext js,jsx,cjs,mjs,ts,tsx,md)

Lint 栈为 ESLint 9.x flat config(eslint.config.mjs),并在 .eslintplugin/index.mjs 定义了三个"渐进式迁移"本地规则:local/no-restricted-types-eventually(基于no-restricted-types)、local/prefer-rest-params-eventuallylocal/prefer-spread-eventually(均直接复用内置规则实现)。Markdown 代码块也参与 lint

CI 会失败的硬规则(Hard rules):

规则说明
graceful-fs,禁止fs/node:fsno-restricted-imports封禁
globalThis,禁止globalno-restricted-globals封禁
源码中 Node 内置模块必须走node:协议(如import * as path from 'node:path'例外expectexpect-utilsjest-matcher-utilsjest-message-utiljest-patternjest-regex-utiljest-util会被 webpack/浏览器端打包消费,不得使用node:前缀,由no-restricted-syntax强制(见 CHANGELOG #16167)
sort-keys源码中键必须字母序,测试中关闭
import-x/order组内字母序(builtin → external → internal → parent → sibling → index),newlines-between: never,可 autofix
禁止Function类型与Boolean/Number/Object/String/Symbol包装类型只用原始类型,local/no-restricted-types-eventually告警
版权头每个.js/.ts/.tsx/.mjs/.cjs必须有如下头部,由yarn check-copyright-headers强制

版权头标准格式:

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */

Prettier 规则直接写在根 package.json 的prettier字段:bracketSpacing: falsesingleQuote: truetrailingComma: 'all'arrowParens: 'avoid'。文档的建议是"让yarn lint:prettier重写,永远不要手排"。

Mock 类型化模式:Always typed / Never

文档给出了 mock 函数的类型化正反例,这是该仓库 TypeScript 实践中最具引用价值的一节:

始终类型化(推荐):

jest.fn<typeof someFn>(); // 显式 jest.fn((arg: T) => result); // 从实现推断 const Mocked = X as jest.MockedClass<typeof X>; // 模块 mock 的类 jest.mocked(obj.method).mockReturnValue(x); // 对已 mock 对象的类型化 cast

绝不要:

jest.fn().mockReturnValue(x); // 会退化为 UnknownFunction (x as jest.Mock).mockReturnValue(y); // "cast 汤" ({foo: jest.fn()}) as unknown as Real; // 应改为构造类型化对象

另外两个容易忽略的点:

  • beforeEach(() => jest.clearAllMocks())会令typecheck:tests失败(返回类型被拓宽),必须改为块体:beforeEach(() => { jest.clearAllMocks(); })
  • 三个 reset 语义的区别:mockClear()只清调用记录;mockReset()再清实现与返回值;mockRestore()进一步恢复原始实现(仅对spyOn有意义)。对应配置项clearMocks/resetMocks/restoreMocks会在每个测试前自动执行相应操作。

参考实现见 packages/jest-mock/typetests/mock-functions.test.ts 与 packages/jest-runtime/src/internals/tests/。

代码模式:DI、封装、注释、错误处理

这一节是文档对贡献者代码风格的强约束,五条规则均可在现有源码中对照:

  1. 类抽取与依赖注入:类超过 3 个依赖时,用*Options命名的构造函数选项袋(不叫*Deps);每个依赖对应一个private readonly x: T字段,不要塞成一个deps: Options字段;方法内直接读字段(this.resolution.resolveCjs(...)而非先解构)。若字段初始化器闭包了this或另一字段,必须在构造函数体内初始化(声明期初始化器先于函数体执行,会捕获undefined)。不要把不相关依赖归入嵌套袋子来减少参数个数——袋子过长说明类职责过多,应拆分;
  2. 封装边界:"顶层消费者看到什么"才是真正重要的边界,内部状态持有类可以保留较松的文件内 API。passthrough 包装看起来多余时,要找语义替代,不能靠暴露它本想隐藏的私有状态来"修";
  3. 命名与注释:禁止缩写名(requireFn而非req);默认不写注释,只为非显性的 WHY(隐藏约束、微妙不变量、特定 bug 的 workaround、反直觉行为)写注释;不解释 WHAT;不在注释里引用"当前 PR/调用方"(会腐烂);在新声明上方插入代码时,检查上一行是否会变成绑定到你声明上的 doc 注释;
  4. 错误处理:用jest-utilisError收窄抛出的值,不要e as Error;不用异常做控制流,优先显式能力谓词而非 try/catch 探针;在系统边界(用户输入、外部 API)校验,内部代码可信;
  5. 重构 PR:不强制严格保持行为不变——暴露潜在 bug 本身就是价值;发现的问题就地修复并补回归测试;review 期间不改写(amend)提交,后续工作以新提交叠加;跨多个逻辑分组的任务,一组一个提交。

How the pieces fit:一次测试运行的完整数据流

文档给出了一段被广泛引用的"自上而下"流程,也是理解 Jest 55 包架构的最快地图:

jest-cli — CLI 参数 jest-config — 加载并归一化用户配置(jest-validate 对照 jest-schemas 校验) jest-core — 编排:haste map、排序、worker 派生、输出 jest-runner — 单 worker 内执行测试 jest-runtime — 模块加载、mock、ESM/CJS 互操作、运行用户代码 jest-circus — 默认测试框架(jest-jasmine2 为遗留替代) expect — 断言;失败信息由 jest-matcher-utils、jest-diff、pretty-format 格式化 jest-reporters — 最终输出(default、GitHub、junit 等)

横向贯穿多阶段的基建包:jest-haste-map(文件爬取+模块映射,Watchman 或 Node fs 两种后端)、jest-resolve(模块解析)、@jest/transform(转译管线,babel-jest为默认)、jest-worker(worker 池)、jest-environment-node/jest-environment-jsdom(VM 上下文+全局)、jest-mockjest.fn/jest.spyOn实现)、jest-fake-timersjest-snapshot/@jest/snapshot-utilsjest-watcher--watch交互)、jest-changed-files/jest-resolve-dependencies--onlyChanged/--onlyFailures)、jest-message-util(堆栈+错误格式化)、jest-types/jest-util(共享类型与小工具,如isErrorinvariantdeepCyclicCopy)。

按目标定位修改点

文档提供的一张"目标 → 入口文件"映射表,是所有修改的导航中枢(下表中的入口文件均已确认存在于仓库):

目标从哪开始可能连带触及
增改 CLI flagjest-cli/src/args.ts(packages/jest-cli/src/args.ts)jest-configjest-types
增改配置项jest-schemas/src/raw-types.ts+jest-config/src/Descriptions.tsjest-validatejest-types、docs/Configuration.md
修改 matcherexpect/src/matchers.ts(或asymmetricMatchers.tsspyMatchers.tsexpect-utilsjest-matcher-utils
修改快照行为jest-snapshot/src/*.ts@jest/snapshot-utilspretty-format
修改 reporter 输出jest-reporters/src/<Reporter>Reporter.tsjest-message-utilpretty-format
修改模块加载/mockjest-runtime/src/index.ts+internals/jest-resolvejest-mock@jest/transform
修改模块解析jest-resolve/src/resolver.ts+defaultResolver.tsjest-runtimejest-haste-map
修改文件爬取/监听jest-haste-map/src/crawlers/watchers/jest-worker
修改转译管线@jest/transform/src/ScriptTransformer.ts各 transformer(如babel-jest
修改测试环境全局jest-environment-node/jest-environment-jsdomjest-runtime(消费方)
修改 worker 调度jest-runner/src/runTest.ts+testWorker.tsjest-worker@jest/test-sequencer
修改定时器 mockjest-fake-timers/src/*.tsjest-runtime(接线)
修改 mock 函数行为jest-mock/src/index.tsjest-runtime(接线)

三条追踪经验

  1. 一次公开 API 行为变更通常需要五处落点:实现包代码、jest-types+jest-schemas的类型、jest-config的归一化、e2e/下验证用户可见行为的 fixture、docs/文档;
  2. Runtime类(packages/jest-runtime/src/index.ts,export default class Runtime)被文档标注为"可子类化",其覆盖缝(override seams)包括requireModulerequireModuleOrMockrequireMockrequireActualrequireInternalModuleunstable_importModule(源码中这些方法分别在 L376-L425 一带定义,且构造函数内大量以回调形式this.requireModule(...)外发)——任何内部"加载模块"的回调都必须经由这些缝分发,不得直接调用兄弟内部方法;
  3. node:vm语义(同步 vs 异步 ESM)成为问题时,查 packages/jest-runtime/src/internals/nodeCapabilities.ts 中的能力门(capability gate),并且代码移动时必须把门控条件原样带走

提交前校验清单与 Yarn 约束

文档给出的推送前完整清单:

yarn build:js yarn eslint --cache --fix <files> # 开发中每次编辑后 yarn lint # 最终检查 yarn jest <affected> # 或 yarn jest --config jest.config.ci.mjs yarn typecheck:tests # 必须 exit 0 yarn check-changelog yarn check-copyright-headers yarn constraints yarn dedupe --check yarn verify-pnp

其中yarn constraints的判定逻辑实现在 yarn.config.cjs,规则包括:同一依赖在所有 workspace 中版本一致(@types/node通过 resolutions 钉在18.x是唯一例外);同一依赖不得同时出现在dependenciesdevDependencies;公开包必须有license/repository/publishConfig/enginesmain/types./开头(私有包则反向清掉这些字段)。yarn constraints --fix可自动修复,yarn dedupe --check查重复版本。新增依赖必须走yarn workspace <pkg> add <dep>——约束会拒绝跨仓库版本不一致。

常见坑速查

文档 "Common pitfalls" 一节的五条速查(第一条与 Setup 一节的双轨机制直接呼应):

  • 仓库包内部报"Module not found":忘了yarn build:js——跨包 import 经main指向build/
  • yarn install后 lockfile 变动:必须提交,CI 使用--immutable(可在 test.yml 的yarn --immutable步骤中印证);
  • typecheck:testsConsole/Stats/__dirname错误:向该测试目录 tsconfig 的types数组加"node"
  • execFilemock 类型化:其重载极多,内部实现无法直接满足typeof execFile;在实现侧 cast 为((_file, _args, cb) => cb(null, ...)) as unknown as typeof execFile
  • peer-dep 警告:存量警告属预期,但新增的不行——合并前必须解决。

CHANGELOG 规范

用户可见变更必须在 CHANGELOG.md 的## main小节下新增条目,分Features/Fixes/Chore & Maintenance三节,条目格式为:

- `[package-name]` 描述 (#PR_NUMBER) - `[jest-core, jest-cli]` 多包条目以逗号分隔包名 (#16100)

要求:同一节内按首个包名字母序排列;yarn check-changelog会校验链接格式(见 scripts/checkChangelog.mjs);一个提交只承载一个逻辑变更,不要 squash 无关工作。当前 CHANGELOG 的## main条目(如mockFn.whenCalledWith特性、describe级重试等)正是这一格式的实例。

遇到不确定时

文档的最后两行是全篇的行动准则:相关包各有CLAUDE.md,包内特有的坑以包级文档为准;以及——"信任代码胜过这份文件"(Trust the code over this file):一旦文档与源码观察冲突,应把修正这份文档作为变更的一部分。这条自我修正条款,正是这类"代理指令文档"能够长期保持准确的关键设计。

【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 3:22:35

重复执行 ALTK-Evolve 任务,TaoToken 换 GPT-4.1 的 Base URL

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 3:22:28

让 Cosmos 软件工厂用 TaoToken 的接口,PR Author 不改业务

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 3:21:19

STM32手写DS1302驱动:时序、BCD码与工程实践详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 3:20:48

tiny11builder 快速上手:6 步把 Windows 11 官方 ISO 瘦成 tiny11.iso

tiny11builder 快速上手&#xff1a;6 步把 Windows 11 官方 ISO 瘦成 tiny11.iso 【免费下载链接】tiny11builder Scripts to build a trimmed-down Windows 11 image. 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny11builder tiny11builder 是一组纯 PowerS…

作者头像 李华