# yunui **Repository Path**: paul-Yellow/yunui ## Basic Information - **Project Name**: yunui - **Description**: vue3 ui lib - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2024-02-21 - **Last Updated**: 2024-11-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # jsx 与 .vue 模板开发选择 > jsx:适合高度复杂可定制化的组件使用 > .vue: 适合简单的、一般的定制化组件使用 # 创建项目 ```sh mkdir vue3-ui-lib cd vue3-ui-lib ``` ## 项目结构 ```tex . ├── docs ├── package.json ├── packages │   ├── components │   │   └── package.json │   ├── theme-chalk │   │   └── package.json │   └── utils │   └── package.json ├── play ├── pnpm-workspace.yaml ├── README.md ├── tsconfig.json └── typings └── vue-shime.d.ts ``` ![monorepo目录结构.png](./monorepo目录结构.png) PS: `-w` 等价于 `--workspace-root` # 创建 package.json ```sh pnpm init ``` ```json { "name": "vue3-ui-lib", "private": true, "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "pnpm -C play dev" }, "keywords": [], "author": "", "license": "ISC" } ``` PS:这里 `"dev": "pnpm -C play dev"` 指定了在项目根目录下执行脚本命令可以执行 play 这个下面的脚本。 # 创建 .npmrc ```sh touch .npmrc ``` ```tex # 使用 pnpm 安装包时提升模块的依赖到 node_modules 下 shamefully-hoist = true ``` # 安装 typescript ```sh pnpm install typescript -D ``` # 创建 tsconfig.json ```sh npx tsc --init ``` ```json { "compilerOptions": { "target": "es2016", // 遵循 ES6 规范 "module": "ESNext", // 打包模块类型 "declaration": false, // 默认打包后不要声明文件 "noImplicitAny": true, // 默认可以 any "removeComments": true, // 移除注释 "moduleResolution": "node", // 安装 node 模块解析 "esModuleInterop": true, // 支持 es、commonjs 模块 "jsx": "preserve", // 告诉 ts 不处理 jsx 语法 "noLib": false, // 不处理库 "sourceMap": true, "lib": [ // 编译时使用的库 "ESNext", "DOM" ], "allowSyntheticDefaultImports": true, // 允许没有导出的模块中导入 "experimentalDecorators": true, // 实验语法,支持装饰器 "forceConsistentCasingInFileNames": true, // 强制区分大小写 "resolveJsonModule": true, // 解析 json 模块 "strict": true, // 严格模式 "skipLibCheck": true // 跳过类库检测 }, "exclude": [ // 不需要解析的文件 "node_modules", "**/__tests__", "dist/**" ] } ``` # 创建 pnpm-workspace.yaml ```sh touch pnpm-workspace.yaml ``` ```yaml packages: - "packages/**" # 存放所有编写的组件 - play # 存放开发测试组件的代码 - docs # 存放组件使用的开发文档 ``` # 创建 typings/vue-shime.d.ts ```sh mkdir typings cd typings touch vue-shime.d.ts ``` ```ts declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } ``` # 在根项目 package.json 中添加我们的组件包依赖 ```sh # 切换到项目根目录执行如下命令,这样就可以在 package.json 查看依赖了 pnpm install @yun/components -w ``` PS: - `-w` 等价 `--workspace-root` - `@yun/components`:我们的组件中 package.json 中配置的 name 属性值名称 # 创建 play 的测试项目 ```sh mkdir play cd play pnpm init pnpm install vue vue-tsc vite @vitejs/plugin-vue -D ``` ## play/package.json ```sh pnpm init ``` ```json { "name": "play", "version": "1.0.0", "description": "", "main": "index.js", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@vitejs/plugin-vue": "^3.0.1", "vite": "^3.0.2", "vue": "^3.2.37" } } ``` ## play/vite.config.ts ```js import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; export default defineConfig({ plugins: [vue()], }); ``` ## play/index.html ```html Document
``` ## play/src/main.ts ```ts import { createApp } from "vue"; import App from "./App.vue"; const app = createApp(App); app.mount("#app"); ``` ## play/src/App.vue ```vue ``` # BEM 类名规范 ```tex block:代码片段 => y-button element:元素 => y-button__element modifier:装饰 => y-button__element--disabled state:状态 => is-checked ``` ## packages/utils/create.ts ```ts /** * BEM 规范 * block:代码片段 => y-button * element:元素 => y-button__element * modifier:装饰 => y-button__element--disabled * state:状态 => is-checked * * y-button * y-button__element * y-button__element--disabled * is-checked */ // z-button-box__element--modifier function _bem( prefixName: string, blockSuffix: string, element: string, modifier: string ) { if (blockSuffix) { prefixName += `-${blockSuffix}`; } if (element) { prefixName += `__${element}`; } if (modifier) { prefixName += `--${modifier}`; } return prefixName; } function createBEM(prefixName: string) { const b = (blockSuffix: string = "") => _bem(prefixName, blockSuffix, "", ""); const e = (element: string = "") => element ? _bem(prefixName, "", element, "") : ""; const m = (modifier: string = "") => modifier ? _bem(prefixName, "", "", modifier) : ""; const be = (blockSuffix: string = "", element: string = "") => blockSuffix && element ? _bem(prefixName, blockSuffix, element, "") : ""; const bm = (blockSuffix: string = "", modifier: string = "") => blockSuffix && modifier ? _bem(prefixName, blockSuffix, "", modifier) : ""; const em = (element: string = "", modifier: string = "") => element && modifier ? _bem(prefixName, "", element, modifier) : ""; const bem = ( blockSuffix: string = "", element: string = "", modifier: string = "" ) => element && modifier && blockSuffix ? _bem(prefixName, blockSuffix, element, modifier) : ""; const is = (name: string, state: boolean) => (state ? `is-${name}` : ""); return { b, e, m, be, bm, em, bem, is, }; } export function createNamespace(name: string) { const prefixName = `y-${name}`; return createBEM(prefixName); } // const bem = createNamespace("icon"); // console.log(bem.b()); // y-icon // console.log(bem.b("box")); // y-icon-box // console.log(bem.e("element")); // y-icon__element // console.log(bem.e()); // 空 // console.log(bem.m("modifier")); // y-icon--modifier // console.log(bem.bem("box", "element", "modifier")); // y-icon-box__element--modifier // console.log(bem.is("checked", true)); // is-checked // console.log(bem.is("checked", false)); // 空 // console.log(bem.be("box", "element")); // y-icon-box__element // console.log(bem.bm("box", "modifier")); // y-icon-box--modifier // console.log(bem.em("element", "modifier")); // y-icon__element--modifier ``` # 第一个组件:Icon ## packages/components/icon/src/icon.vue ```vue ``` **注意:**这里使用了 script 的 setup 写法,不能自定义组件名,那如何解决呢? - 方案一:不推荐 在对应的组件中添加脚本 ```vue + ``` - 方案二:推荐 1. 配置插件:`unplugin-vue-define-options` ```sh pnpm install unplugin-vue-define-options -D -w ``` 2. 配置 vite.config.ts ```ts import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; +// 解决setup编写组件时, 自定义组件名 +import defineOptions from "unplugin-vue-define-options/vite"; export default defineConfig({ plugins: [ vue(), + defineOptions() ], }); ``` 3. 在 .vue 的文件组件中调用 defineOptions 来指定组件名 ```vue ``` ## packages/components/icon/src/icon.ts ```ts import { ExtractPropTypes, PropType } from "vue"; export const iconProps = { color: String, size: [Number, String] as PropType, } as const; export type IconProps = ExtractPropTypes; ``` ## packages/components/icon/index.ts ```ts import YIcon from "./src/icon.vue"; export default YIcon; export * from "./src/icon"; ``` ## 使用 Icon 组件 在 play 测试项目中,使用 Icon 组件 ### play/src/App.vue ```vue ``` ## 通过 vue.use() 注册我们的组件 ### packages/components/icon/index.ts ```ts import { Plugin } from "vue"; import _Icon from "./src/icon.vue"; //#region export type SFCWithInatall = T & Plugin; /** * 为组件添加 install 方法,便于 vue.use 使用该组件 * @param comp 组件对象 * @returns 组件 */ export function withInstall(comp: T) { (comp as SFCWithInatall).install = function (app) { // 全局注册组件 app.component((comp as unknown as { name: string }).name, comp); }; return comp as SFCWithInatall; } //#endregion export const YIcon = withInstall(_Icon); export default YIcon; export * from "./src/icon"; // 为了在template模板中使用组件时,有对应的 ts 提示功能 declare module "vue" { export interface GlobalComponents { YIcon: typeof YIcon; } } ``` #### volar 配合在 template 模板中使用组件有代码提示 ```ts // 为了在模板中使用组件时,有对应的 ts 提示功能 declare module "vue" { export interface GlobalComponents { YIcon: typeof YIcon; } } ``` ### play/src/main.ts ```ts import { createApp } from "vue"; import App from "./App.vue"; +import YIcon from "@yun/components/icon"; const app = createApp(App); +const plugins = [YIcon]; +plugins.forEach((plugin) => app.use(plugin)); app.mount("#app"); ``` ### play/src/App.vue ```vue ``` # VitePress 编写文档 ## 安装 vitepress ```sh mkdir docs cd docs pnpm init pnpm install vitepress -D ``` ## docs/package.json ```json { "name": "docs", "scripts": { "dev": "vitepress dev ." }, "devDependencies": { "vitepress": "1.0.0-alpha.4" } } ``` **项目根目录 package.json 配置执行脚本** ```json "scripts": { + "docs:dev": "pnpm -C docs dev" }, ``` ## 首页配置 **docs/index.md** ```markdown --- layout: home hero: name: yun-ui 组件库 text: 基于 Vue 3 的组件库. tagline: 掌握 Vue3 组件编写 actions: - theme: brand text: 快速开始 link: /guide/quickStart features: - icon: 🛠️ title: 组件库构建流程 details: Vue3 组件库构建 ... - icon: ⚙️ title: 组件库单元测试 details: Vue3 组件库测试 ... --- ``` ## vite.config.ts **docs/vite.config.ts** ```ts import { defineConfig } from "vite"; // 解决在 script 的 setup 中定义组件名: defineOptions({name: "YIcon"}) import DefineOptions from "unplugin-vue-define-options/vite"; export default defineConfig({ plugins: [DefineOptions()], }); ``` ## 文档配置 **docs/ .vitepress/config.js** ```js module.exports = { title: "YUN-UI", description: "Vue3 YUN UI", themeConfig: { lastUpdated: "最后更新时间", docsDir: "docs", editLinks: true, editLinkText: "编辑此网站", repo: "https://gitee.com/login", footer: { message: "Released under the MIT License.", copyright: "Copyright © 2022-present Timly", }, nav: [ { text: "指南", link: "/guide/installation", activeMatch: "/guide/" }, { text: "组件", link: "/component/icon", activeMatch: "/component/" }, ], sidebar: { "/guide/": [ { text: "指南", items: [ { text: "安装", link: "/guide/installation" }, { text: "快速开始", link: "/guide/quickStart" }, ], }, ], "/component/": [ { text: "基础组件", items: [{ text: "Icon", link: "/component/icon" }], }, ], }, }, }; ``` ## 注册组件 > 在 vitepress 中注册我们的组件 **docs/ .vitepress/theme/index.js** ```js import DefaultTheme from "vitepress/theme"; import YIcon from "@yun/components/icon"; import "@yun/theme-chalk/src/index.scss"; console.log(YIcon); export default { ...DefaultTheme, enhanceApp({ app }) { app.use(YIcon); // 在 vitepress 中,注册全局组件 }, }; ``` ## guide **docs/guide/installation.md** ```markdown # 安装 ``` **docs/guide/quickStart.md** ```markdown # 快速开始 ``` ## 组件文档 > 编写我们的组件文档 **docs/component/icon.md** ````markdown # Icon 图标 yun-ui 推荐使用 [xicons](https://www.xicons.org/#/) 作为图标库。 ```sh pnpm install @vicons/material ``` ## 使用图标 - 如果你想像用例一样直接使用,你需要全局注册组件,才能够直接在项目里使用。 ```vue ``` ## API ### Icon Props | 名称 | 类型 | 默认值 | 说明 | | ----- | ---------------- | --------- | -------- | | color | string | undefined | 图标颜色 | | size | number \| string | undefined | 图标大小 | ```` # Eslint ```sh npx eslint --init ``` ## 安装依赖 ```sh pnpm install eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest eslint@latest -D -w ``` ```sh # 支持 vue 的 typescript 的 eslint 配置 pnpm install @vue/eslint-config-typescript -D -w ``` ## .eslintrc.js ```js module.exports = { env: { browser: true, es2021: true, node: true, }, extends: [ "eslint:recommended", "plugin:vue/vue3-recommended", // https://eslint.vuejs.org/ "plugin:@typescript-eslint/recommended", "@vue/typescript/recommended", ], parserOptions: { ecmaVersion: "latest", parser: "@typescript-eslint/parser", sourceType: "module", }, plugins: ["vue", "@typescript-eslint"], rules: { "vue/html-self-closing": "off", "vue/singleline-html-element-content-newline": "off", "vue/multi-word-component-names": "off", "vue/prefer-import-from-vue": "off", }, globals: { defineOptions: "readonly", }, }; ``` ## .eslintignore ```tex node_modules dist *.css *.jpg *.jpeg *.png *.gif *.d.ts ``` # prettier ## .prettierrc.js ```js module.exports = { singleQuote: false, // 使用双引号 semi: true, // 使用分号 trailingComma: 'none', // 末尾逗号 arrowParens: 'avoid', // 箭头函数括号 endOfLine: 'auto' // 结尾换行自动 } ``` ## .prettierignore ```tex node_modules dist ``` # 编辑器 ## .editorconfig ```tex root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf ``` # 打包 packages/theme-chalk 样式 ## 安装 ```sh pnpm install gulp @types/gulp sucrase -D -w pnpm install gulp-sass gulp-autoprefixer gulp-clean-css sass @types/gulp-sass @types/sass @types/gulp-autoprefixer @types/gulp-clean-css -D -w ``` ## packages.json - 配置项目根目录下 package.json 的执行脚本 ```json "scripts": { "dev": "pnpm -C play dev", "docs:dev": "pnpm -C docs dev", + "build": "gulp -f build/gulpfile.ts" }, ``` ## build/gulpfile.ts ```ts import { series } from "gulp"; import { run, withTaskName } from "./tools"; // 清除根目录下已经打包的 dist const cleanDist = withTaskName("clean", async () => run("rm -rvf ./dist")); // 执行 packages 目录中 package.json 配置有 build 的对应命令脚本 const buildPackages = withTaskName("buildPackages", async () => // 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug run("pnpm run --filter ./packages --parallel build") ); export default series(cleanDist, buildPackages); ``` ## build/tools/index.ts ```ts import { spawn } from "child_process"; import { projectRoot } from "./paths"; export const withTaskName = (name: string, fn: T) => Object.assign(fn, { displayName: name }); export const run = (command: string) => { return new Promise((resolve, rejected) => { const [cmd, ...args] = command.split(" "); // 执行命令 const app = spawn(cmd, args, { cwd: projectRoot, stdio: "inherit", // 将子进程输出共享给父进程 shell: true }); app.on("close", resolve); }); }; ``` ## build/tools/paths.ts ```ts import path from "path"; export const projectRoot = path.resolve(__dirname, "../../"); ``` ## packages/theme-chalk/package.json ```json "scripts": { + "build": "gulp" }, ``` ## packages/theme-chalk/gulpfile.ts ```ts import path from "path"; import { series, src, dest } from "gulp"; import gulpSass from "gulp-sass"; import sass from "sass"; import gulpCleanCss from "gulp-clean-css"; import autoPrefixer from "gulp-autoprefixer"; function r(p: string) { return path.resolve(__dirname, p); } // 编译 sass 到 css const complieSassToCss = () => { return src(r("./src/*.scss")) .pipe(gulpSass(sass).sync()) .pipe(autoPrefixer()) .pipe(gulpCleanCss()) .pipe(dest("./dist/css")); }; // 拷贝字体图标 const copyFont = () => { return src(r("./src/font/**")).pipe(dest("./dist/fonts")); }; // 把编译好的 css 拷贝到根目录下 dist 中 const copyFullStyle = () => { return src(r("./dist/**")).pipe(dest(r("../../dist/theme-chalk"))); }; // 注意执行顺序 export default series(complieSassToCss, copyFont, copyFullStyle); ``` # 打包 packages/utils 工具 ## 安装 ```sh pnpm install gulp-typescript -D -w ``` ## build/tools/config.ts ```ts import { resolve } from "path"; import { outDir } from "./paths"; export const buildConfig = { esm: { module: "ESNext", // tsconfig 输出的结果 ES6 模块 format: "esm", // 需要配置格式化化后的模块规范 output: { name: "es", // 打包到 dist 目录下的那个目录 path: resolve(outDir, "es") }, bundle: { path: "yun-ui/es" } }, cjs: { module: "CommonJS", format: "cjs", output: { name: "lib", path: resolve(outDir, "lib") }, bundle: { path: "yun-ui/lib" } } }; export type BuildConfig = typeof buildConfig; ``` ## build/tools/path.ts ```ts import path from "path"; export const projectRoot = path.resolve(__dirname, "../../"); + export const outDir = path.resolve(__dirname, "../../dist"); ``` ## build/packages.ts ```ts import path from "path"; import { src, series, dest, parallel } from "gulp"; import ts from "gulp-typescript"; import { withTaskName } from "./tools"; import { buildConfig } from "./tools/config"; import { outDir, projectRoot } from "./tools/paths"; export function buildPackages(dirname: string, name: string) { const tasks = Object.entries(buildConfig).map(([module, config]) => { const output = path.resolve(dirname, config.output.name); const build = withTaskName(`build:${name || dirname}`, () => { const tsConfig = path.resolve(projectRoot, "tsconfig.json"); // ts的配置文件的路径 const inputs = ["**/*.ts", "!gulpfile.ts", "!node_modules"]; return src(inputs) .pipe( ts.createProject(tsConfig, { declaration: true, // 需要生成声明文件 strict: false, module: config.module })() ) .pipe(dest(output)); }); const copy = withTaskName(`copy:${name || dirname}`, () => { // 将 utils 模块编译 ts 后的 js 拷贝到 dist 目录下的 es 目录和 lib 目录 return src(`${output}/**`).pipe( dest(path.resolve(outDir, config.output.name, name)) ); }); return series(build, copy); }); // 将 ts -> js, 也可以使用 rullup 处理 return parallel(...tasks); } ``` ## packages/utils/packages.json ```json "scripts": { + "build": "gulp" }, ``` ## packages/utils/gulpfile.ts ```ts import { buildPackages } from "../../build/packages"; export default buildPackages(__dirname, "utils"); ``` # 打包 packages/components 组件 ## 安装 ```sh pnpm install rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-vue rollup-plugin-typescript2 -D -w ``` ## 打包为整个组件文件 ### build/gulpfile.ts ```ts import { series } from "gulp"; import { run, withTaskName } from "./tools"; // 清除根目录下已经打包的 dist const cleanDist = withTaskName("clean", async () => run("rm -rf ./dist")); // 执行 packages 目录中 package.json 配置有 build 的对应命令脚本 const buildPackages = withTaskName("buildPackages", async () => // 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug run("pnpm run --filter ./packages --parallel build") ); +// 处理 packages/components 的所有组件成一个单文件 +const buildAllComponent = withTaskName("buildFullComponent", async () => + // 会去找 full-component.ts 中 buildFullComponent + run("pnpm run build buildFullComponent") +); // buildFullComponent 函数在 full-component.ts 中 +export * from "./full-component"; export default series( cleanDist, buildPackages, + buildAllComponent ); ``` ### build/tools/paths.ts ```ts import path from "path"; export const projectRoot = path.resolve(__dirname, "../../"); export const outDir = path.resolve(__dirname, "../../dist"); +export const yunRoot = path.resolve(__dirname, "../../packages/yun"); ``` ### build/full-component.ts ```ts import path from "path"; import fs from "fs/promises"; import vue from "rollup-plugin-vue"; import typescript from "rollup-plugin-typescript2"; import { nodeResolve } from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; import { parallel } from "gulp"; import { OutputOptions, rollup } from "rollup"; import { outDir, yunRoot } from "./tools/paths"; const buildFull = async () => { console.log("-----------------buildFull------------------------"); // rollup打包的配置信息 const config = { input: path.resolve(yunRoot, "index.ts"), // 打包的入口 plugins: [nodeResolve(), typescript(), vue(), commonjs()], external: (id: string) => /^vue/.test(id) // 排除打包的时候不需要打包 vue 代码 }; const buildConfig = [ { format: "umd", file: path.resolve(outDir, "index.js"), name: "yun", // 全局的名字 exports: "named", // 导出的名字用命名的方式导出 liraryTarget:"var" var name = "xxx" globals: { // 表示使用的 vue 是全局的 vue: "Vue" } }, { format: "esm", file: path.resolve(outDir, "index.esm.js") } ]; const bundle = await rollup(config); return Promise.all( buildConfig.map(config => bundle.write(config as OutputOptions)) ); }; export const buildFullComponent = parallel(buildFull); ``` ## 打包成多个组件文件(支持引入组件库时可以按需加载) ### 安装 ```sh pnpm install fast-glob -D -w ``` ### build/gulpfile.ts ```ts import { series } from "gulp"; import { run, withTaskName } from "./tools"; // 清除根目录下已经打包的 dist const cleanDist = withTaskName("clean", async () => run("rm -rf ./dist")); // 执行 packages 目录中 package.json 配置有 build 的对应命令脚本 const buildPackages = withTaskName("buildPackages", async () => // 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug run("pnpm run --filter ./packages --parallel build") ); // 处理 packages/components 的所有组件成一个文件 const buildAllComponent = withTaskName("buildFullComponent", async () => // 会去找 full-component.ts 中 buildFullComponent run("pnpm run build buildFullComponent") ); +// 引用我们组件时,打包按需加载的多个文件组件 +const buildSingleComponent = withTaskName("buildComponent", () => + // 会去找 component.ts 中 buildComponent + run("pnpm run build buildComponent") +); // buildFullComponent 函数在 full-component.ts 中 export * from "./full-component"; +// buildComponent 函数在 component.ts 中 +export * from "./component"; export default series( cleanDist, buildPackages, buildAllComponent, + buildSingleComponent ); ``` ### build/tools/paths.ts ```ts import path from "path"; export const projectRoot = path.resolve(__dirname, "../../"); export const outDir = path.resolve(__dirname, "../../dist"); export const yunRoot = path.resolve(__dirname, "../../packages/yun"); +export const componentRoot = path.resolve(projectRoot, "packages/components"); ``` ### build/tools/index.ts ```ts import { spawn } from "child_process"; import { projectRoot } from "./paths"; export const withTaskName = (name: string, fn: T) => Object.assign(fn, { displayName: name }); export const run = (command: string) => { return new Promise((resolve, rejected) => { const [cmd, ...args] = command.split(" "); // 执行命令 const app = spawn(cmd, args, { cwd: projectRoot, stdio: "inherit", // 将子进程输出共享给父进程 shell: true }); app.on("close", resolve); }); }; +export const pathRewriter = (format: string) => { + return (id: string) => { + id = id.replaceAll("@yun", `yun/${format}`); + return id; + }; +}; ``` ### build/component.ts ```ts import path from "path"; import { series, parallel } from "gulp"; import { sync } from "fast-glob"; import vue from "rollup-plugin-vue"; import typescript from "rollup-plugin-typescript2"; import { nodeResolve } from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; import { OutputOptions, rollup } from "rollup"; import { componentRoot } from "./tools/paths"; import { buildConfig } from "./tools/config"; import { pathRewriter } from "./tools"; const buildEachComponent = async () => { console.log("------------------buildEachComponent-------------------"); // 打包每个组件 const files = sync("*", { cwd: componentRoot, onlyDirectories: true }); // 分别把 components 文件夹下的组件,放到 dist/es/components 下 和 dist/lib/compmonents const builds = files.map(async (file: string) => { // 每个组件的入口 const input = path.resolve(componentRoot, file, "index.ts"); const config = { input, plugins: [nodeResolve(), vue(), typescript(), commonjs()], external: (id: string) => /^vue/.test(id) || /^@yun/.test(id) // 排除引入包那些不需要打包 }; const bundle = await rollup(config); const options = Object.values(buildConfig).map(config => ({ format: config.format, file: path.resolve(config.output.path, `components/${file}/index.js`), paths: pathRewriter(config.output.name) // @yun => yun/es yun/lib // 视情况,是否配置,否在会包报警告 Use `output.exports: "named"` to disable this warning // name: "yun", // 全局的名字 // exports: "named" // 导出的名字用命名的方式导出 liraryTarget:"var" var name = "xxx" })); await Promise.all( options.map(option => bundle.write(option as OutputOptions)) ); }); return Promise.all(builds); }; export const buildComponent = series(buildEachComponent); ``` ## 解析 .vue 文件中 指定 lang="ts" 的代码 ### 安装 ```sh pnpm install ts-morph -D -w ``` ### 生成 types 类型文件 .d.ts ##### build/component.ts ```ts import path from "path"; import fs from "fs/promises"; import { series, parallel } from "gulp"; import glob, { sync } from "fast-glob"; import vue from "rollup-plugin-vue"; import typescript from "rollup-plugin-typescript2"; import { nodeResolve } from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; import { OutputOptions, rollup } from "rollup"; import { Project, SourceFile } from "ts-morph"; import * as VueCompiler from "@vue/compiler-sfc"; import { componentRoot, outDir, projectRoot } from "./tools/paths"; import { buildConfig } from "./tools/config"; import { pathRewriter, run } from "./tools"; const buildEachComponent = async () => { console.log("------------------buildEachComponent---------------------"); // 打包每个组件 const files = sync("*", { cwd: componentRoot, onlyDirectories: true }); // 分别把 components 文件夹下的组件,放到 dist/es/components 下 和 dist/lib/compmonents const builds = files.map(async (file: string) => { // 每个组件的入口 const input = path.resolve(componentRoot, file, "index.ts"); const config = { input, plugins: [nodeResolve(), vue(), typescript(), commonjs()], external: (id: string) => /^vue/.test(id) || /^@yun/.test(id) // 排除引入包那些不需要打包 }; const bundle = await rollup(config); const options = Object.values(buildConfig).map(config => ({ format: config.format, file: path.resolve(config.output.path, `components/${file}/index.js`), paths: pathRewriter(config.output.name), // @yun => yun/es yun/lib // 视情况,是否配置,否在会包报警告 Use `output.exports: "named"` to disable this warning name: "yun", // 全局的名字 exports: "named" // 导出的名字用命名的方式导出 liraryTarget:"var" var name = "xxx" })); await Promise.all( options.map(option => bundle.write(option as OutputOptions)) ); }); return Promise.all(builds); }; // 生成 ts 的 type 文件 +const genTypes = async () => { + console.log("----------------genTypes--------------------"); + + const project = new Project({ + // 生成 .d.ts,需要有一个 tsconfig.json + compilerOptions: { + allowJs: true, + declaration: true, + emitDeclarationOnly: true, + noEmitOnError: true, + outDir: path.resolve(outDir, "types"), + baseUrl: projectRoot, + paths: { + "@yun/*": ["packages/*"] + }, + skipLibCheck: true, + strict: false + }, + tsConfigFilePath: path.resolve(projectRoot, "tsconfig.json"), + skipAddingFilesFromTsConfig: true + }); + + const filePaths = await glob("**/*", { + // ** 任意目录 * 任意文件 + cwd: componentRoot, + onlyFiles: true, + absolute: true + }); + + const sourceFiles: SourceFile[] = []; + + await Promise.all( + filePaths.map(async function (file) { + if (file.endsWith(".vue")) { + const content = await fs.readFile(file, "utf8"); + const sfc = VueCompiler.parse(content); + const { script } = sfc.descriptor; + + if (script) { + const content = script.content; // 拿到脚本 icon.vue.ts => icon.vue.d.ts + const sourceFile = project.createSourceFile(file + ".ts", content); + sourceFiles.push(sourceFile); + } + } else { + // 把所有的 ts 文件都放在一起发射成 .d.ts 文件 + const sourceFile = project.addSourceFileAtPath(file); + sourceFiles.push(sourceFile); + } + }) + ); + + await project.emit({ + emitOnlyDtsFiles: true // 默认是放到内存中的 + }); + + const tasks = sourceFiles.map(async (sourceFile: any) => { + const emitOutput = sourceFile.getEmitOutput(); + const tasks = emitOutput.getOutputFiles().map(async (outputFile: any) => { + const filepath = outputFile.getFilePath(); + await fs.mkdir(path.dirname(filepath), { + recursive: true + }); + // @yun -> yun/es -> .d.ts 不用去 lib 下查找 + await fs.writeFile(filepath, pathRewriter("es")(outputFile.getText())); + }); + await Promise.all(tasks); + }); + + await Promise.all(tasks); +}; export const buildComponent = series( buildEachComponent, + genTypes ); ``` ### 对应的组件生成对应的类型文件 .d.ts ##### build/component.ts ```ts const copyTypes = () => { console.log("----------------copyTypes--------------------"); const src = path.resolve(outDir, "types/components/"); const copy = (module: string) => { const output = path.resolve(outDir, module, "components"); return () => run(`cp -r ${src}/* ${output}`); }; return parallel(copy("es"), copy("lib")); }; ``` **注意:还要保证每个组件的 index.js 的入口存在** ```ts // build/component.ts const buildComponentEntry = async () => { console.log("----------------buildComponentEntry--------------------"); const config = { input: path.resolve(componentRoot, "index.ts"), plugins: [typescript()], external: () => true }; const bundle = await rollup(config); return Promise.all( Object.values(buildConfig) .map(config => ({ format: config.format, file: path.resolve(config.output.path, "components/index.js") })) .map(config => bundle.write(config as OutputOptions)) ); }; ``` ### 打包组件库入口 yun 生成入口文件 index.js 和类型文件 .d.ts #### build/full-component.ts 生成入口文件 index.js ````ts const buildEntry = async () => { console.log("-----------------buildEntry------------------------"); const entryFiles = await fs.readdir(yunRoot, { withFileTypes: true }); const entryPoints = entryFiles .filter(f => f.isFile()) .filter(f => !["package.json"].includes(f.name)) .map(f => path.resolve(yunRoot, f.name)); const config = { input: entryPoints, plugins: [nodeResolve(), vue(), typescript()], external: (id: string) => /^vue/.test(id) || /^@yun/.test(id) }; const bundle = await rollup(config); return Promise.all( Object.values(buildConfig) .map(config => ({ format: config.format, dir: config.output.path, paths: pathRewriter(config.output.name), // 视情况,是否配置,否在会包报警告 Use `output.exports: "named"` to disable this warning name: "yun", // 全局的名字 exports: "named" // 导出的名字用命名的方式导出 liraryTarget:"var" var name = "xxx" })) .map(option => bundle.write(option as OutputOptions)) ); }; ```` #### build/entry-types.ts 生成 yun 入口文件的类型文件 .d.ts ```ts import path from "path"; import fs from "fs/promises"; import glob from "fast-glob"; import { ModuleKind, Project, ScriptTarget, SourceFile } from "ts-morph"; import { outDir, projectRoot, yunRoot } from "./tools/paths"; import { parallel, series } from "gulp"; import { run, withTaskName } from "./tools"; import { buildConfig } from "./tools/config"; const genEntryTypes = async () => { console.log("---------------genEntryTypes------------------------"); const files = await glob("*.ts", { cwd: yunRoot, absolute: true, onlyFiles: true }); const project = new Project({ compilerOptions: { declaration: true, module: ModuleKind.ESNext, allowJs: true, emitDeclarationOnly: true, noEmitOnError: false, outDir: path.resolve(outDir, "entry/types"), target: ScriptTarget.ESNext, rootDir: yunRoot, strict: false }, skipFileDependencyResolution: true, tsConfigFilePath: path.resolve(projectRoot, "tsconfig.json"), skipAddingFilesFromTsConfig: true }); const sourceFiles: SourceFile[] = []; files.map(f => { const sourceFile = project.addSourceFileAtPath(f); sourceFiles.push(sourceFile); }); await project.emit({ emitOnlyDtsFiles: true }); const tasks = sourceFiles.map(async sourceFile => { const emitOutput = sourceFile.getEmitOutput(); for (const outputFile of emitOutput.getOutputFiles()) { const filepath = outputFile.getFilePath(); await fs.mkdir(path.dirname(filepath), { recursive: true }); await fs.writeFile( filepath, outputFile.getText().replaceAll("@yun", "."), "utf8" ); } }); await Promise.all(tasks); }; const copyEntryTypes = () => { const src = path.resolve(outDir, "entry/types"); const copy = (module: any) => parallel( withTaskName(`copyEntryTypes:${module}`, async () => { const p = path.resolve( outDir, (buildConfig as any)[module].output.path ); return run(`cp -r ${src}/* ${p}/`); }) ); return parallel(copy("esm"), copy("cjs")); }; export const genTypes = series(genEntryTypes, copyEntryTypes()); ``` #### build/gulpfile.ts ```ts import { series, parallel } from "gulp"; +import { genEntryTypes } from "./entry-types"; import { run, withTaskName } from "./tools"; // 清除根目录下已经打包的 dist const cleanDist = withTaskName("clean", async () => run("rm -rf ./dist")); // 执行 packages 目录中 package.json 配置有 build 的对应命令脚本 const buildPackages = withTaskName("buildPackages", async () => // 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug run("pnpm run --filter ./packages --parallel build") ); // 处理 packages/components 的所有组件成一个文件 const buildAllComponent = withTaskName("buildFullComponent", async () => // 会去找 full-component.ts 中 buildFullComponent run("pnpm run build buildFullComponent") ); // 引用我们组件时,打包按需加载的多个文件组件 const buildSingleComponent = withTaskName("buildComponent", () => // 会去找 component.ts 中 buildComponent run("pnpm run build buildComponent") ); // buildFullComponent 函数在 full-component.ts 中 export * from "./full-component"; // buildComponent 函数在 component.ts 中 export * from "./component"; export default series( cleanDist, buildPackages, buildAllComponent, buildSingleComponent, + parallel(genEntryTypes) ) ``` ### 拷贝 package.json #### build/gulpfile.ts ```ts import { series, parallel } from "gulp"; import { genTypes } from "./entry-types"; import { run, withTaskName } from "./tools"; +import { outDir, yunRoot } from "./tools/paths"; // 清除根目录下已经打包的 dist const cleanDist = withTaskName("clean", async () => run("rm -rf ./dist")); // 执行 packages 目录中 package.json 配置有 build 的对应命令脚本 const buildPackages = withTaskName("buildPackages", async () => // 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug run("pnpm run --filter ./packages --parallel build") ); // 处理 packages/components 的所有组件成一个文件 const buildAllComponent = withTaskName("buildFullComponent", async () => // 会去找 full-component.ts 中 buildFullComponent run("pnpm run build buildFullComponent") ); // 引用我们组件时,打包按需加载的多个文件组件 const buildSingleComponent = withTaskName("buildComponent", () => // 会去找 component.ts 中 buildComponent run("pnpm run build buildComponent") ); // buildFullComponent 函数在 full-component.ts 中 export * from "./full-component"; // buildComponent 函数在 component.ts 中 export * from "./component"; +// 拷贝 package.json +const copySourceCode = () => async () => { + await run(`cp ${yunRoot}/package.json ${outDir}/package.json`); +}; export default series( cleanDist, buildPackages, buildAllComponent, buildSingleComponent, parallel(genTypes, + copySourceCode()) ); ```