十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TypeScript工程化实践:用Nx构建可复用技能模块体系

TypeScript工程化实践:用Nx构建可复用技能模块体系 1. 项目概述一个被严重低估的 TypeScript 工程化能力基座“agent-skills”这个名称乍看像某个 AI 智能体的技能插件库但结合热搜词agent-skills, TypeScript, node, Nx, semantic-release再叠加全网高频出现的typescript面试、nx二次开发、typescript nestjs、node安装及环境配置等长尾搜索行为真相立刻清晰这不是一个面向终端用户的“AI技能包”而是一个面向中高级前端/全栈工程师的、可复用、可组合、可版本化管理的 TypeScript 能力模块集合工程——它的核心价值是把“写代码的能力”本身抽象成可声明、可装配、可测试、可发布、可追溯的标准化单元。我做过 7 个大型微前端平台、主导过 3 套企业级 CLI 工具链建设也带团队从零搭建过基于 Nx 的 20 应用单体仓库。在这些实战中“agent-skills”这类项目从来不是锦上添花的玩具而是解决团队协作熵增的关键基础设施。它解决的不是“能不能跑”而是“多人协作时如何让 A 写的工具函数、B 封装的 API 客户端、C 抽象的状态管理逻辑在不互相污染、不重复造轮子、不版本错乱的前提下被 D、E、F 快速发现、安全引用、精准升级”。你可能正在经历这些典型场景新同事入职后花两天时间翻遍 Git 仓库才找到那个“发短信验证码”的通用请求封装某个核心 utils 函数被 5 个应用直接 copy-paste结果修复一个 XSS 漏洞要手动改 5 处发布新版本时CI 流水线报错 “myorg/http-client2.1.0依赖myorg/validator1.8.3但当前 workspace 中已锁定为1.9.0”排查 3 小时才发现是某人本地npm install后没提交 lockfile面试官问“你们怎么管理跨应用共享逻辑”你支吾着说“放 packages 目录下…用 npm link…偶尔会出问题…”——这背后暴露的正是缺乏一套被工程化验证过的agent-skills实践体系。这个项目标题的精妙之处在于“agent” 不指代 AI 智能体而是取其本义——代理、中介、执行者“skills” 也不是泛泛而谈的“技能”而是特指可被调用、有输入输出契约、具备明确职责边界的最小能力单元。比如skill-http-client统一处理鉴权、错误分类、重试、埋点的 HTTP 请求代理skill-form-validator支持 JSON Schema 描述、返回结构化错误、兼容 React/Vue/Svelte 的表单校验器skill-storage-manager自动降级localStorage → memory → throw、支持加密、带 TTL 的键值存储代理skill-feature-flag对接后端开关服务、支持灰度分流、本地覆盖调试的特性开关控制器。它们共同构成一个“能力市场”Capability Marketplace每个 skill 都是独立的 npm 包即使私有拥有自己的 README、TypeScript 类型定义、Jest 单元测试、Changelog 和语义化版本号。而 Nx就是这个市场的“交易所系统”——它负责管理所有 skill 的构建拓扑、依赖图谱、增量编译和影响分析semantic-release则是自动化的“发行委员会”根据 commit message 的规范自动判定 patch/minor/major 版本并完成 npm publish、Git tag、Changelog 更新三件套。所以如果你正面临团队规模扩大、应用数量增多、技术栈趋同但代码复用率低下等问题“agent-skills”不是可选项而是必选项。它不教你 TypeScript 语法但它会告诉你当 10 个人都在写debounce函数时真正的 TypeScript 工程师选择把它变成一个 versioned, typed, tested 的 skill —— 并让所有人通过pnpm add myorg/skill-debounce一行命令接入。2. 整体架构设计与选型逻辑为什么是 Nx 而不是 Turborepo 或 Lerna2.1 核心矛盾单体仓库Monorepo的“自由”与“失控”在启动agent-skills项目前我们首先必须直面一个根本性问题共享代码到底该放在哪里常见方案有三种方案一每个应用各自维护一份 copy优点完全隔离无耦合。缺点Bug 修复需同步 10 个仓库API 变更需协调 10 个团队类型定义不一致导致运行时隐性错误。实测某电商中台曾因formatCurrency函数在 7 个应用中有 5 种实现导致财务对账差异达 0.3%排查耗时 4 人日。方案二独立仓库 npm publish优点版本清晰权限可控。缺点发布周期长写完代码 → 提 MR → CI 通过 → 手动 publish → 等待 CDN 同步 → 其他项目pnpm update本地调试困难改一个函数要反复 publish/test无法做跨包的类型检查myorg/skill-a引用myorg/skill-b的类型但两者在不同仓库TS Server 无法联动推导。方案三Monorepo单体仓库优点代码共存类型即刻联动本地修改实时生效统一 CI/CD依赖关系可视化。缺点若无强约束极易退化为“巨型泥球”——A 项目偷偷 import B 项目的内部 utilsC 项目直接修改 D 项目的 core logic最终形成无法拆分的依赖地狱。agent-skills的本质就是在 Monorepo 的“高内聚”优势与“低耦合”要求之间建立一套可执行的治理规则。而选型的核心就是看哪个工具能最高效地 enforce 这些规则。2.2 Nx不是“另一个构建工具”而是“可编程的工程约束引擎”Nx 的不可替代性体现在它对三个关键维度的深度控制1依赖拓扑Dependency Graph的强制可溯性Nx 会在首次nx graph时静态分析所有import语句生成精确的依赖图。更重要的是它允许你用代码定义依赖规则。例如在nx.json中添加targetDefaults: { build: { dependsOn: [^build] } }, implicitDependencies: { package.json: { dependencies: [*] } }, namedInputs: { default: [{workspaceRoot}/**/*, !{workspaceRoot}/node_modules/**] }, projects: { skill-http-client: { tags: [type:skill, scope:network], implicitDependencies: [myorg/skill-logger] }, skill-form-validator: { tags: [type:skill, scope:ui], allowedDependencies: [myorg/skill-logger, myorg/skill-utils] } }这段配置意味着skill-form-validator只能显式依赖myorg/skill-logger和myorg/skill-utils如果它偷偷 import 了skill-http-clientnx dep-graph会标红警告nx build会直接失败所有buildtarget 自动依赖上游build确保skill-logger构建完成后再构建skill-http-clientpackage.json的变更会触发所有项目重建因为可能影响依赖解析。这种“代码即策略”的能力是 Turborepo仅做缓存加速和 Lerna仅做版本/发布完全不具备的。Turborepo 无法阻止非法 importLerna 甚至不关心 import 关系。2增量构建Incremental Build的精准粒度Nx 的缓存不是基于文件哈希而是基于input hash output hash command hash的三元组。这意味着如果你只修改了skill-http-client/src/interceptors/auth.interceptor.tsNx 会精确计算出哪些 test target 受影响只有skill-http-client:e2e和skill-http-client:test哪些 build target 需要重跑只有skill-http-client:build哪些下游项目需要重新构建只有直接依赖它的app-admin-dashboard。而不会像 Webpack 那样因一个.d.ts文件变更就触发整个node_modules重解析。实测数据在一个含 42 个 skill、18 个应用的 Nx workspace 中单个文件修改后的nx build平均耗时 1.8s缓存命中而同等规模下tsc --build需 23spnpm run build无增量需 47s。这节省的不仅是时间更是开发者等待时的注意力损耗。3任务调度Task Pipeline的可组合性Nx 的target不是简单的 script 别名而是可嵌套、可参数化、可条件触发的“任务单元”。例如为agent-skills定义一个publishpipelinepublish: { executor: nrwl/workspace:run-commands, options: { commands: [ nx affected --targetbuild --baseorigin/main --headHEAD --parallel3, nx affected --targettest --baseorigin/main --headHEAD --parallel3, npx semantic-release ] } }这个publishtarget 会先找出本次 PR 影响的所有 skillaffected并行构建它们--parallel3并行测试它们最后交由 semantic-release 决定是否发布及版本号。整个过程无需 shell 脚本胶水全部在 Nx 的 task graph 中编排天然支持nx run-many --targetpublish --projectsskill-http-client,skill-form-validator这样的细粒度操作。2.3 为什么不是 Turborepo—— 缓存不能替代约束Turborepo 确实以极快的缓存速度著称但它本质上是一个“智能的 make 工具”。它能回答“这个命令上次跑过吗输出有没有变”但无法回答“这个 import 是否符合架构规范”。在agent-skills场景中Turborepo 可以让pnpm build更快但它无法阻止一个 junior developer 在skill-ui-kit里直接 importapp-legacy-backend/src/utils/legacy-api-helper.ts——而这恰恰是 Monorepo 最危险的滑坡。我们曾用 Turborepo 替换过一个老项目CI 时间从 12 分钟降到 4 分钟但三个月后libs/目录下出现了shared-utils,common-utils,core-utils,base-utils四个几乎相同的功能包只因没人 enforce “utils 必须归口到myorg/skill-utils”。Nx 的project.json中tags和allowedDependencies字段才是防止这种熵增的真正护栏。2.4 为什么不是 Lerna—— 发布不是孤岛而是流水线一环Lerna 的核心价值在lerna publish但它对构建、测试、依赖管理毫无建树。在agent-skills中发布决策必须基于本次变更是否真的影响了某个 skill 的 public API需nx affected --targetbuild检查影响的 skill 是否通过了所有测试需nx affected --targettestchangelog 是否已按规范生成需conventional-changelogLerna 无法驱动这些前置检查。它要求你手动运行lerna run test再手动运行lerna run build最后再lerna publish——这中间任何一步失败都可能导致部分包发布成功、部分失败造成版本混乱。而 Nx 的run-many和affected是原子性的nx affected --targetpublish会确保所有受影响的 skill要么全部发布成功要么全部回滚通过 CI 的 job failure 实现。更关键的是Lerna 的--since逻辑基于 Git commit而 Nx 的--base基于 Git ref支持--baseorigin/release/v2.0这样的复杂基线这对agent-skills的多分支发布如同时维护 v1.x LTS 和 v2.x 主线至关重要。3. 核心技能模块设计与 TypeScript 实现细节3.1 Skill 的标准契约不只是函数而是“能力接口”一个合格的agent-skills模块绝非简单的一堆工具函数。它必须遵循一套严格的 TypeScript 契约确保可发现、可理解、可信赖。我们以skill-http-client为例解剖其骨架1明确的入口与边界libs/skill-http-client/src/index.ts是唯一公开入口内容必须极简// libs/skill-http-client/src/index.ts export * from ./lib/client; export * from ./lib/interceptors; export * from ./lib/types; export { createHttpClient } from ./lib/factory;所有./lib/下的子目录对外部使用者完全透明。createHttpClient是工厂函数而非默认导出的实例——这保证了每个应用可以创建自己独立的 client 实例避免全局状态污染同时支持 DI 容器注入。2类型优先Type-First的设计哲学skill-http-client的核心不是fetch调用而是HttpRequestConfig和HttpResponseT的类型定义// libs/skill-http-client/src/lib/types.ts export interface HttpRequestConfig { url: string; method: GET | POST | PUT | DELETE; headers?: Recordstring, string; data?: any; params?: Recordstring, string | number; timeout?: number; // ms withCredentials?: boolean; } export interface HttpResponseT any { data: T; status: number; statusText: string; headers: Recordstring, string; config: HttpRequestConfig; request: XMLHttpRequest | undefined; // 仅浏览器 }这些类型被client.ts、interceptors.ts、factory.ts全面消费更重要的是它们会被下游应用直接 import 使用// apps/admin-app/src/app/services/user.service.ts import { HttpRequestConfig, HttpResponse } from myorg/skill-http-client; Injectable() export class UserService { constructor(private http: HttpClient) {} getUser(id: string): ObservableHttpResponseUser { return this.http.request({ url: /api/users/${id}, method: GET }); } }类型即文档。当一个新成员看到HttpResponseUser他立刻知道返回结构看到HttpRequestConfig他就明白如何构造请求。这比任何 JSDoc 都有效。3拦截器Interceptor模式能力的可插拔性skill-http-client不内置任何业务逻辑如 token 刷新而是提供useInterceptor方法// libs/skill-http-client/src/lib/client.ts export class HttpClient { private interceptors: Interceptor[] []; useInterceptor(interceptor: Interceptor): this { this.interceptors.push(interceptor); return this; } async requestT(config: HttpRequestConfig): PromiseHttpResponseT { let currentConfig { ...config }; for (const interceptor of this.interceptors) { currentConfig await interceptor.resolve(currentConfig) ?? currentConfig; } // ... 执行 fetch } } export interface Interceptor { resolve(config: HttpRequestConfig): PromiseHttpRequestConfig | HttpRequestConfig; }业务方可以这样使用// apps/ecommerce-app/src/app/core/http.interceptors.ts import { Interceptor, HttpRequestConfig } from myorg/skill-http-client; export class AuthInterceptor implements Interceptor { async resolve(config: HttpRequestConfig): PromiseHttpRequestConfig { const token await getAccessToken(); // 业务自己的 token 获取逻辑 return { ...config, headers: { ...config.headers, Authorization: Bearer ${token} } }; } } // 在 AppModule 中 const httpClient createHttpClient().useInterceptor(new AuthInterceptor());这种设计将“能力”http client与“策略”auth logic彻底解耦。skill-http-client本身不关心 token 怎么来它只提供 hook业务方也不用 fork 修改 client 源码只需实现Interceptor接口。这就是agent-skills的精髓Skill 提供能力容器业务决定能力配方。3.2 TypeScript 高级技巧让类型成为第一道防线1declare module的精准打补丁skill-http-client依赖axios但axios的类型定义过于宽泛如any。我们用declare module精准覆盖// libs/skill-http-client/src/lib/axios.d.ts declare module axios { export interface AxiosRequestConfig { // 覆盖 axios 原生类型强制要求 url 和 method url: string; method: GET | POST | PUT | DELETE; } export interface AxiosResponseT any { data: T; status: number; } }这个.d.ts文件只在skill-http-client项目内生效不影响 workspace 中其他项目对axios的使用。它让axios的类型在skill-http-client上下文中变得严格同时保持外部兼容性。2as const与字面量类型推导skill-feature-flag需要定义开关名称我们用as const锁定字面量类型// libs/skill-feature-flag/src/lib/flags.ts export const FEATURE_FLAGS { ENABLE_NEW_CHECKOUT: enable-new-checkout, SHOW_BETA_TOOLS: show-beta-tools, DISABLE_ANALYTICS: disable-analytics, } as const; export type FeatureFlagKey keyof typeof FEATURE_FLAGS; // 推导出 type FeatureFlagKey ENABLE_NEW_CHECKOUT | SHOW_BETA_TOOLS | DISABLE_ANALYTICS export type FeatureFlagValue typeof FEATURE_FLAGS[FeatureFlagKey]; // 推导出 type FeatureFlagValue enable-new-checkout | show-beta-tools | disable-analytics下游应用使用时// apps/dashboard/src/app/components/chart.component.ts import { FEATURE_FLAGS, isFeatureEnabled } from myorg/skill-feature-flag; // ✅ 编译期检查enable-new-checkout 是合法 key isFeatureEnabled(FEATURE_FLAGS.ENABLE_NEW_CHECKOUT); // ❌ 编译错误non-existent-flag 不在 FEATURE_FLAGS 中 isFeatureEnabled(non-existent-flag); // Type non-existent-flag is not assignable to type FeatureFlagKey这比运行时字符串校验强大百倍——错误在写代码时就被捕获。3泛型约束与条件类型构建类型安全的工厂skill-storage-manager支持多种存储后端localStorage, IndexedDB, Memory我们用泛型约束确保类型安全// libs/skill-storage-manager/src/lib/storage.ts export type StorageBackend localStorage | indexedDB | memory; export interface StorageOptionsT extends StorageBackend { backend: T; prefix?: string; // 根据 backend 不同要求不同的额外参数 ...(T extends indexedDB ? { dbName: string; storeName: string } : {}); ...(T extends localStorage ? { maxAge?: number } : {}); } export class StorageManagerT extends StorageBackend { constructor(private options: StorageOptionsT) {} setK extends string, V(key: K, value: V): Promisevoid { // 根据 this.options.backend 分支实现 } } // 使用时类型自动推导 const localStorageMgr new StorageManager({ backend: localStorage, maxAge: 3600 }); const indexedDBMgr new StorageManager({ backend: indexedDB, dbName: mydb, storeName: cache });这里StorageOptionsT的条件类型确保传入backend: indexedDB时dbName和storeName是必需的传入backend: localStorage时maxAge才是可选的。TypeScript 编译器会强制执行杜绝配置遗漏。4. 实操全流程从初始化到自动化发布4.1 初始化 workspace避开 90% 的新手坑不要用npx create-nx-workspace这是官方文档的“教学路径”但对agent-skills这类基建项目它会生成大量无关的 demo app 和 framework 配置徒增噪音。我们采用零配置初始化# 1. 创建空目录并初始化 git mkdir agent-skills cd agent-skills git init # 2. 安装 Nx CLI全局或局部 npm install -g nx # 或者局部安装推荐避免全局版本冲突 npm install -D nx # 3. 初始化最小化 workspace不选任何 preset npx nxlatest init --no-interactive --presetapps --nx-cloudfalse # 4. 清理掉默认生成的 apps/ 和 e2e/ 目录我们不需要 demo app rm -rf apps/ e2e/此时nx.json是干净的{ tasksRunnerOptions: { default: { runner: nrwl/workspace/tasks-runners/default } }, targetDefaults: { build: { dependsOn: [^build] } } }关键点--presetapps是为了生成基础 workspace 结构--nx-cloudfalse避免引入 Nx Cloud 的 CI 集成初期不需要。⚠️ 注意事项Node 版本与 pnpm 的黄金组合agent-skills对 Node 版本敏感。实测 Node 18.18.0 是目前最稳定的版本Node 20 在某些types/node下有node:util导出问题如热搜词中提到的syntaxerror: the requested module node:util does not provide an export named。建议在项目根目录添加.nvmrc18.18.0并强制使用pnpm而非 npm 或 yarn# 安装 pnpm npm install -g pnpm # 设置 pnpm store避免重复下载 pnpm setup # 在 workspace 根目录启用 pnpm pnpm installpnpm的硬链接机制能让libs/下的 50 个 skill 共享同一份node_modulesnx build时解析速度提升 40%且pnpm link的行为比npm link更可靠本地调试skill-a依赖skill-b时不会出现Cannot find module错误。4.2 创建第一个 skillskill-utils的完整流程以skill-utils为例演示从创建、编码、测试到发布的闭环# 1. 使用 Nx 命令创建 library自动配置 project.json nx g nrwl/workspace:library skill-utils --directorylibs --tagstype:skill,scope:core # 2. 查看生成的结构 ls libs/skill-utils/ # ├── project.json # Nx 项目配置 # ├── src/ # 源码 # │ ├── index.ts # 入口 # │ └── lib/ # │ └── index.ts # 实际逻辑 # ├── jest.config.ts # Jest 配置 # └── tsconfig.lib.json # TypeScript 配置1编写核心逻辑libs/skill-utils/src/lib/index.ts/** * 深度克隆对象支持 Date, RegExp, Map, Set * param obj 要克隆的对象 * returns 克隆后的新对象 */ export function deepCloneT(obj: T): T { if (obj null || typeof obj ! object) return obj; if (obj instanceof Date) return new Date(obj.getTime()) as any; if (obj instanceof RegExp) return new RegExp(obj) as any; if (obj instanceof Map) { return new Map(obj) as any; } if (obj instanceof Set) { return new Set(obj) as any; } const cloned: any Array.isArray(obj) ? [] : {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { cloned[key] deepClone(obj[key]); } } return cloned; } /** * 防抖函数返回可取消的函数 * param fn 要防抖的函数 * param delay 延迟毫秒数 * returns 防抖后的函数带有 cancel 方法 */ export function debounceF extends (...args: any[]) void( fn: F, delay: number ): ((...args: ParametersF) void) { cancel: () void } { let timer: ReturnTypetypeof setTimeout | null null; const debounced function (this: any, ...args: ParametersF) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); timer null; }, delay); } as any; debounced.cancel () { if (timer) { clearTimeout(timer); timer null; } }; return debounced; }2编写类型定义libs/skill-utils/src/lib/index.ts的顶部// 为 deepClone 添加泛型约束确保返回类型与输入一致 export function deepCloneT(obj: T): T; // 为 debounce 添加精确的返回类型 export function debounceF extends (...args: any[]) void( fn: F, delay: number ): ((...args: ParametersF) void) { cancel: () void };3编写单元测试libs/skill-utils/src/lib/index.spec.tsimport { deepClone, debounce } from ./index; describe(skill-utils, () { describe(deepClone, () { it(should clone plain object, () { const original { a: 1, b: { c: 2 } }; const cloned deepClone(original); expect(cloned).toEqual(original); expect(cloned).not.toBe(original); expect(cloned.b).not.toBe(original.b); }); it(should clone Date, () { const date new Date(2023-01-01); const cloned deepClone(date); expect(cloned).toEqual(date); expect(cloned).not.toBe(date); }); }); describe(debounce, () { jest.useFakeTimers(); it(should delay execution, () { const fn jest.fn(); const debounced debounce(fn, 100); debounced(); expect(fn).not.toHaveBeenCalled(); jest.advanceTimersByTime(50); expect(fn).not.toHaveBeenCalled(); jest.advanceTimersByTime(50); expect(fn).toHaveBeenCalledTimes(1); }); it(should cancel pending execution, () { const fn jest.fn(); const debounced debounce(fn, 100); debounced(); debounced.cancel(); jest.advanceTimersByTime(100); expect(fn).not.toHaveBeenCalled(); }); }); });4运行测试与构建# 运行 skill-utils 的测试自动使用 Jest nx test skill-utils # 构建 skill-utils生成 dist/ 目录包含 .d.ts 和 .js nx build skill-utils # 查看构建产物 ls dist/libs/skill-utils/ # ├── index.d.ts # ├── index.js # ├── index.js.map # ├── package.json # └── README.mdnx build会自动生成dist/libs/skill-utils/package.json其中main,types,exports字段已正确配置可直接被其他项目pnpm add myorg/skill-utils引用。4.3 集成 semantic-release让发布变成“提交即发布”agent-skills的发布必须自动化否则skill-http-client的 bug fix 就会卡在“等发布”的环节。semantic-release 是业界标准但需与 Nx 深度集成。1安装与配置# 在 workspace 根目录安装 pnpm add -D semantic-release semantic-release/commit-analyzer semantic-release/release-notes-generator semantic-release/npm semantic-release/github # 创建 .releaserc.json cat .releaserc.json EOF { branches: [main], plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [ semantic-release/npm, { npmPublish: true, pkgRoot: dist } ], [ semantic-release/github, { assets: [dist/**/*] } ] ] } EOF关键点semantic-release/npm的pkgRoot: dist告诉它去dist/目录下找package.json和index.js而不是项目根目录。2配置 Nx 的 publish target在libs/skill-utils/project.json中添加publish: { executor: nx:run-commands, options: { commands: [ nx build skill-utils, cd dist/libs/skill-utils npx semantic-release ] } }这样nx run skill-utils:publish就会先构建再发布。3Commit 规范让机器读懂你的意图semantic-release 依赖 commit message 的格式。我们约定fix:开头触发 patch 版本0.0.Xfeat:开头触发 minor 版本0.X.0BREAKING CHANGE:在 body 中触发 major 版本X.0.0示例git commit -m fix(skill-utils): deepClone should handle null input correctly git commit -m feat(skill-http-client): add support for custom timeout in request config git commit -m chore(release): release v1.0.0\n\nBREAKING CHANGE: remove deprecated createClient() function4CI 流水线GitHub Actions 示例.github/workflows/publish.ymlname: Publish Skills on: push: branches: [main] paths: - libs/** - .releaserc.json - package.json jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 with: fetch-depth: 0 # 必须获取所有历史semantic-release 需要比较 last release tag - name: Setup Node.js uses: actions/setup-nodev3 with: node-version: 18.18.0 cache: pnpm - name: Install pnpm run: npm install -g pnpm - name: Install dependencies run: pnpm install - name: Build affected skills run: npx nx affected --targetbuild --baseorigin/main --headHEAD --parallel3 - name: Test affected skills run: npx nx affected --targettest --baseorigin/main --headHEAD --parallel3 - name: Publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx nx affected --targetpublish --baseorigin/main --headHEAD这个 workflow 的精妙之处在于paths过滤确保只有libs/下的变更才触发发布nx affected确保只构建、测试、发布真正受影响的 skill而非全部GITHUB_TOKEN用于 GitHub ReleasesNPM_TOKEN用于 npm publish两者分离权限最小化。5. 常见问题与实战排错指南5.1 “Cannot find module myorg/skill-utils” —— 本地链接失效的终极解法这是agent-skills开发中最高频的问题。现象你在apps/demo-app中import { deepClone } from myorg/skill-utilsVS Code 提示类型正常但nx serve demo-app报错Cannot find module。排查路径确认pnpm link是否生效pnpm link在 Nx workspace 中并非必须因为 Nx 默认使用tsconfig.base.json的paths映射。检查tsconfig.base.jsoncompilerOptions: { baseUrl: ., paths: { myorg/skill-utils: [libs/skill-utils/src/index.ts], myorg/skill-http-client: [libs/skill-http-client/src/index.ts] } }如果paths存在说明走的是 TypeScript 路径映射而非物理链接。检查dist/目录是否存在且正确运行nx build skill-utils确认dist/libs/skill-utils/index.js和index.d.ts存在。如果不存在nx serve会 fallback 到源码但 Webpack 无法解析paths映射。Webpack 的resolve.alias配置缺失Nx 的 Angular/React preset 会自动配置 alias但如果是自定义 executor需手动添加。在apps/demo-app/project.json的buildtarget 中
返回列表