使用 PNPM 从零搭建 Monorepo,测试组件并发布
1 目标
通过 PNPM 创建一个 monorepo(多个项目在一个代码仓库)项目,形成一个通用的仓库模板。
这里以在该 monorepo 项目中搭建 web components 类型的组件库为例,介绍从仓库搭建、组件测试到组件发布的整个流程。
这个仓库既可以用于公司存放和管理所有的项目,也可以用于将个人班余的所有积累整合其中。
如不想一步一步搭建,可以直接下载 项目模板。
2 环境要求
核心是 PNPM 和 Node.js,没有特殊的版本要求,只要他俩能对应上即可。

当前项目使用的 PNPM 版本为 9.3.0,Node.js 为 18.20.3。
除了以上两个,项目中也使用到了以下工具或插件,可以按需添加,如不使用则不用考虑其环境要求。
vite(v5.2.0):主要用于项目运行和构建,要求 Node.js v18+ 或者 v20+。
Storybook(v8.1.7):用于组件的测试和展示,要求 Vite v4.0 +。
3 仓库搭建
3.1 新建项目
新建一个文件夹作为项目容器。
这里起名叫 ease-life,意为轻松生活。所有的学习、工作都是为了更好地、更轻松的生活。
3.2 创建目录
3.2.1 apps
在项目根目录下创建 apps 文件夹。
在 apps 下创建 storybook 文件夹。用于测试和展示自定义的 web components。
apps 文件夹主要存放应用程序,如:Storybook、VitePress,还可以加上 vue-test、react-test 来对 web components 做测试。
3.2.2 packages
在项目根目录下创建 apps 文件夹。
在 packages 下分别创建 config(配置信息)、web-components(实现组件与框架无关) 文件夹。
- 在 config 文件下创建 eslint、stylelint、commitlint 以及 typescript,用于存放对应的配置
- 在 web-components 创建 text 文件夹,实现一个简单的文本组件。 text 文件夹下创建 src 文件夹。
packages 底下主要包含插件、组件、命令行、类库等,除了以上的内容还可以按需加上 vue-components、react-components、cli、map-library 等等。
形成的目录结构如下:
ease-life
|-- apps
| |-- storybook
|-- packages|-- config| |-- commitlint| |-- eslint| |-- stylelint| |-- typescript|-- web-components|-- text|-- src
3.3 添加文件
3.3.1 PNPM 相关
- 在项目根目录下添加文件:pnpm-workspace.yaml,定义 PNPM 的工作空间:
packages:# 匹配 packages 目录下(任意文件夹下)的所有模块- 'packages/**'# 匹配 apps 直接子文件夹下的所有模块- 'apps/*'
这里的模块,说的是:包含 package.json,可以被发布到 NPM 远程仓库的项目。
- 在项目根目录下添加文件:.npmrc,定义 PNPM 的配置项:
# 允许链接工作空间中的包
link-workspace-packages = true# 在引用工作空间中的包时,设置前缀为 *,即:使用最新版本的包
save-prefix = ''
3.3.2 Vite 相关
- 在根目录下运行以下内容:
pnpm init
从而生成 package.json,如下:
{"name": "ease-life","version": "1.0.0","description": "","main": "index.js","scripts": {"test": "echo \"Error: no test specified1\" && exit 1"},"keywords": [],"author": "","license": "ISC",
}
- 在 web-components 以及 web-components/text 下都执行 pnpm init,或直接将根目录下的 package.json 拷贝过去。
本文的目的是要每个组件都能够被单独被发布至 NPM 仓库,如:@ease-life/text。如只需要做整个组件库的发布,则无需在 web-components/text 下执行 pnpm init。
- 在项目最外层空间下添加 vite:
pnpm add vite -Dw
packages 里的所有模块如无特殊情况,统一使用 vite 来运行、打包,因此只需要在项目最外层安装一次即可。
- 在项目根目录下,添加文件 vite.config.js:
import { defineConfig } from 'vite'export default defineConfig({build: {lib: {entry: 'index.ts',fileName: 'index'},}
})
- 修改之前生成的 package.json:
{"name": "ease-life","version": "1.0.0","description": "哥的幸福生活全靠你啦","scripts": {"dev": "vite --open","build": "vite build","preview": "vite preview --open"},"keywords": ["monorepo","web components","pnpm","storybook","changeset"],"author": "zqc","repository": {"type": "git","url": ""},"license": "MIT","type": "module","devDependencies": {"vite": "^5.2.0"},"engines": {"node": ">= 18.20.3","pnpm": ">= 9.3.0"}
}
3.3.3 添加 config
待完善
3.3.4 添加其他
- 在项目跟目录下添加 .gitignore:
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*node_modules
dist
dist-ssr
*.local# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
3.3.5 自定义 Web Components
- 在 packages/web-components/text/src 下创建 text.ts:
import { html, css, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';@customElement('el-text')
export class ELText extends LitElement {static styles = css`p { color: blue }`;@property()name = 'Somebody';render() {return html`<p>Hello, ${this.name}!</p>`;}
}
- 在 packages/web-components/text 下创建 index.ts(导出当前组件):
export { ELText as default } from './src/text.ts';
- 在 packages/web-components/text 下添加 tsconfig.json:
{"compilerOptions": {"target": "ESNext","experimentalDecorators": true,"useDefineForClassFields": false,"module": "ESNext","lib": ["ES2020","DOM","DOM.Iterable"],"skipLibCheck": true,/* Bundler mode */"moduleResolution": "bundler","allowImportingTsExtensions": true,"resolveJsonModule": true,"isolatedModules": true,"noEmit": true,/* Linting */"strict": true,"noUnusedLocals": true,"noUnusedParameters": true,"noFallthroughCasesInSwitch": true},"include": ["src"]
}
以上内容将会被移至 packages/config/typescript 中,待修改
- 修改 在 packages/web-components/text 下的 package.json:
{"name": "@ease-life/text","version": "1.0.0","description": "","type": "module","files": ["dist"],"main": "./dist/index.umd.cjs","module": "./dist/index.js","exports": {".": {"import": "./dist/index.js","require": "./dist/index.umd.cjs"}},"scripts": {"build": "vite build -c ../../../vite.config.js"},"keywords": ["ELText"],"author": "","license": "ISC","dependencies": {"lit": "^3.1.2"}
}
3.4 生成 storybook
- 在 apps/storybook 文件夹的路径下运行以下内容:
pnpm dlx storybook@latest init
选择最后一个选项,回车。

此时就会在 apps/storybook 下有对应的 storybook 的内容。
- 删除 apps/storybook/src/stories 下自带的 button.css、Button.stories.ts、Button.ts、header.css、Header.stories.ts、Header.ts、page.css、Page.stories.ts、Page.ts 六个文件。
最终项目文件目录结构如下:
ease-life|-- .gitignore|-- .npmrc|-- package.json|-- pnpm-lock.yaml|-- pnpm-workspace.yaml|-- vite.config.js|-- apps| |-- package.json| |-- storybook| |-- .gitignore| |-- index.html| |-- package.json| |-- tsconfig.json| |-- .storybook| | |-- main.ts| | |-- preview.ts| |-- public| | |-- vite.svg| |-- src| |-- index.css| |-- my-element.ts| |-- vite-env.d.ts| |-- assets| | |-- lit.svg| |-- stories| |-- Configure.mdx| |-- Text.stories.ts| |-- assets| |-- accessibility.png| |-- accessibility.svg| |-- addon-library.png| |-- assets.png| |-- avif-test-image.avif| |-- context.png| |-- discord.svg| |-- docs.png| |-- figma-plugin.png| |-- github.svg| |-- share.png| |-- styling.png| |-- testing.png| |-- theming.png| |-- tutorials.svg| |-- youtube.svg|-- packages|-- config| |-- commitlint| |-- eslint| |-- stylelint| |-- typescript|-- map-library|-- web-components|-- text|-- index.ts|-- package.json|-- tsconfig.json|-- src|-- text.ts
4 组件测试
- 在项目根目录下运行以下内容,来对 text 进行构建:
pnpm -F @ease-life/text build
会在 packages/web-components/text 下生成 dist 文件夹,里边有 index.js(ESM) 以及 index.umd.cjs(CommonJS)。
- 在 apps/storybook/src/stories 下添加一个 Text.stories.ts:
import type { Meta, StoryObj } from '@storybook/web-components';
import '@ease-life/text';const meta: Meta = {component: 'el-text'
};export default meta;
type Story = StoryObj;export const Default: Story = {args: {name: 'world',},
};
- 修改 apps/storybook 下的 package.json,将其中的 name 改为:
"name": "@ease-life/storybook",
- 在项目根目录下运行以下内容来安装刚才定义的 web components:
pnpm -F @ease-life/storybook add @ease-life/text
- 在项目根目录下运行以下内容,来启动 storybook:
pnpm -F @ease-life/storybook storybook
在浏览器中显示以下内容,则证明组件没有问题了。

5 组件发布
5.1 在 NPM 官网注册
如果没有注册过,则打开 NPM,点击右上角的 Sign Up,按提示填入信息。
5.2 登录账户
注册完后直接登录。
5.3 创建组织
打开创建组织的页面,在其中添加组织名称,组织名称就是 scope 的名称,也就是这里 @ 后面的内容。
@ease-life/tex,我这里创建了 ease-life 的组织。
5.3 组件发布
- 用户登录,在项目根目录下运行:
pnpm login
看到提示后,再次回车,在浏览器弹出的页面中进行登录,成功后显示以下内容:

- 组件发布,在项目根目录下运行:
pnpm publish -r
会自动发布仓库中版本发生改变的组件。

出现以上类似内容,就证明发布成功了。
相关文章:
使用 PNPM 从零搭建 Monorepo,测试组件并发布
1 目标 通过 PNPM 创建一个 monorepo(多个项目在一个代码仓库)项目,形成一个通用的仓库模板。 这里以在该 monorepo 项目中搭建 web components 类型的组件库为例,介绍从仓库搭建、组件测试到组件发布的整个流程。 这个仓库既可…...
Oracle 19C 数据库表被误删除的模拟恢复
Oracle 19C 数据库表被误删除的模拟恢复操作 1、模拟创建表用于恢复测试 sqlplus zzh/zzh SQL> create table obj_tb tablespace users as select * from dba_objects; Table created. SQL> select count(*) from obj_tb; COUNT(*) ---------- 72373 2、记录当前…...
【CICID】GitHub-Actions语法
[TOC] 【CICID】GitHub-Actions语法 1 场景 当我们开发过程中,经常需要提交代码,打包,部署新代码到对应的环境,整个过程都是人工手动操作,占据开发人员大量时间,并且很繁琐容易出错。所以需要借助一些…...
Ionic 创建 APP
Ionic 创建 APP Ionic 是一个强大的开源框架,用于构建高性能、高质量的移动和网页应用程序。它结合了 Angular、React 或 Vue 的强大功能,以及 Capacitor 或 Cordova 的原生功能,使得开发者可以轻松地创建跨平台的应用程序。本篇文章将指导您如何使用 Ionic 创建一个基本的…...
【数学代码】幂
Hello!大家好,我是学霸小羊,今天来讲讲幂。 求几个相同因数的积的运算,叫做乘方,乘方的结果叫做幂。 a^n,读作 “ a的n次方 ” 或 “ a的n次方幂”,a叫做底数,n叫做指数。 对于底数、指数和幂…...
os.system() 函数
os.system() 是 Python 标准库 os 模块中的一个函数,用于在子终端中运行系统命令。它可以在 Python 脚本中调用外部命令或程序。具体来说,它通过执行命令字符串并返回执行状态来实现这一点。下面是对 os.system() 函数的详细解释: import os…...
Spring Boot中的RESTful API详细介绍及使用
在Spring Boot中,RESTful API的实现通过控制器类中的方法和特定的注解来完成。每个注解对应不同的HTTP请求方法,并通过处理请求参数和返回响应来实现不同的操作。 下面将详细解释RESTful API中的各个方面,包括GetMapping, PostMapping, PutMa…...
nlp学习笔记
目录 很多入门例子 bert chinese 很多入门例子 https://github.com/lansinuote/Huggingface_Toturials bert chinese import torch import torch.nn as nn from transformers import AutoTokenizer, AutoModel, BertModel, TFBertModel, BertTokenizer# youpath = D:/bert-…...
使用python获取内存信息
#!/usr/bin/python # -*- coding:utf-8 -*- psutil模块是一个跨平台的获取进程和系统应用情况(CPU,内存,磁盘,网络,传感器)的库。 该模块用于系统监控、限制进程资源和运行进程的管理等方面。 内存信息&am…...
外包公司泛滥,这些常识你应该提前知道?
今年大环境确实很不好 很多985,211的应届生都在网上大吐苦水,很多大龄离职大厂的技术人也好,业务人也好,都纷纷转向短视频平台做起了自媒体。而找工作的人普遍发现,某最火的招聘平台几乎都被外包公司刷屏了。大大小小的外包公司如…...
Linux下的抓包工具使用介绍
应用层 传输层 网络层 数据链路层 物理层 1)tcpdump(传输/网络层) tcpdump -i eth0 tcpdump -i eth0 -vnn -v:显示包含有TTL,TOS值等等更详细的信息 -n:不要做IP解析为主机名 -nn:…...
centos环境上:k8s 简单安装教程
本次演示安装3节点k8s环境,无需多言,直接上操作步骤: 1、环境准备 k8s部署前,首先需要准备好环境,除了1.4 步骤,其他步骤在所有(3个)节点上都要执行: 1.1 关闭防火墙 s…...
短视频矩阵系统/源码搭建---拆解热门视频功能开发上线
短视频矩阵系统/源码搭建 一、短视频矩阵系统源码开发需要用到以下技术: 1.前端技术:HTML、CSS、JavaScript、Vue.js等前端框架。 2.后端技术:Java、Python、PHP等后端语言及相关框架,如Spring Boot、Django、Laravel等。 3.移…...
手机和模拟器的 Frida 环境配置
目录 一、配置 JDK 和 android 环境 二、连接设备和查看权限 1、连接设备 2、查看手机权限 三、手机配置 Frida 1、frida-server下载 2、验证 四、模拟器配置 Frida 1、下载模拟器并调节成手机版: 2、连接并查看架构 3、配置并开启 x86 的 frida-serve…...
力扣1385.两个数组间的距离值
力扣1385.两个数组间的距离值 二分判断答案是否正确 class Solution {public:int findTheDistanceValue(vector<int>& arr1, vector<int>& arr2, int d) {ranges::sort(arr2);ranges::sort(arr1);int m arr2.size();auto check [&](int low,int h…...
[C++] 小游戏 斗破苍穹 2.11.6 版本 zty出品
大家好,今天zty带来的是斗破苍穹的 2.11.6 版本,这个版本主要更新了:1、背包 2、将退出游戏改到了设置里面 3、如果不逃跑不会停止战斗。废话不多说, 先赞后看 养成习惯 code #include<stdio.h> #include<iostrea…...
认识与学习JSP
JSP核心技术 什么是JSP JSP全称是Java Server Pages,它和servle技术一样,都是SUN公司定义的一种用于开发动态web资源的技术。JSP/Servlet规范。JSP实际上就是Servlet JSP这门技术的最大的特点在于,写jsp就像在写html,但它相比htm…...
MySql 各种 join
MySql 定义了很多join的方式,接下来我们用一个例子来讲解。 用到的表 本文用到了两个表s1,s2: 内外连接 测试 1 1 1.select * from s1 inner join s2 on(s1.id s2.id);: -------- | id | id | -------- | 3 | 3 | | 4 | 4 | --------2…...
【Android面试八股文】Android中操作多线程的方式有哪些?
文章目录 1. 使用 `Thread` 和 `Runnable`2. `AsyncTask`3. `Handler` 和 `Looper`4. `HandlerThread`5. `ThreadPoolExecutor`6. `IntentService`7. `RxJava`8. `Coroutine`(协程)9. `WorkManager`在Android开发中,有多种方式可以进行多线程操作。以下是主要的几种方式: 1…...
语义分割和目标检测的关系
目录 1.语义分割的目标 2.目标检测的目标 3.两种任务的异同之处 从大方向的任务特点上来说 (1)物体的位置 (2)物体的分类 从数据格式来说 (1)语义分割的数据格式 (2)目标检测的数据格式 1.语义分…...
以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
大模型多显卡多服务器并行计算方法与实践指南
一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南 在数字化营销时代,邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天,我们将深入解析邮件打开率、网站可用性、页面参与时…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...
springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...
前端中slice和splic的区别
1. slice slice 用于从数组中提取一部分元素,返回一个新的数组。 特点: 不修改原数组:slice 不会改变原数组,而是返回一个新的数组。提取数组的部分:slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...
HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散
前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说,在叠衣服的过程中,我会带着团队对比各种模型、方法、策略,毕竟针对各个场景始终寻找更优的解决方案,是我个人和我司「七月在线」的职责之一 且个人认为,…...
