第 4 章:包工程基础

这一章讲 sag-icons 的 npm 包骨架——package.json 关键字段、TypeScript 双层配置、为什么选 Vite 库模式。

你可能觉得这些都是"npm 包常识",没什么好讲的。但 sag-icons 有几个有意的设计值得拎出来:

  • exports 字段精确控制子路径导入
  • tsconfig.json 和 tsconfig.build.json 用途分离
  • vite.config.ts 选了 ES-only,放弃 CJS
  • CSS 以字符串导出,让运行时自动注入

读完这一章你会知道为什么 sag-icons 的 package.json 长那个样子,以及怎么抄。


4.1package.json 全文解读

sag-icons 的 package.json:

package.json
{
  "name": "@jujiuyey/sag-icons",
  "version": "0.0.1",
  "description": "Framework-agnostic SVG icon library with multi-tone color support (Web Components)",
  "type": "module",
  "main": "./dist/index.js",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "dev": "rspress dev",
    "build": "rspress build",
    "build:lib": "tsc -p tsconfig.build.json && vite build",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "@rspress/plugin-shiki": "^1.47.1",
    "@types/react": "^18.3.31",
    "@types/react-dom": "^18.3.7",
    "rspress": "^1.47.2",
    "typescript": "^5.9.3",
    "vite": "^6.4.3"
  },
  "keywords": [
    "icon",
    "svg",
    "web-components",
    "custom-elements",
    "framework-agnostic"
  ],
  "license": "MIT"
}

对应文件:package.json

下面逐个字段拆。


type: "module"

"type": "module"

声明这个包是 ESM。所有 .js 文件默认按 ES Module 解析,而不是 CommonJS。

这对 sag-icons 来说没问题,因为:

  • 现代打包器(Vite / Webpack 5 / Rspack)原生支持 ESM
  • 用户在 React / Vue 项目里 import "@jujiuyey/sag-icons",ESM 完全够用
  • 不发 CJS 少一半配置麻烦

代价是不支持特别老的工具链(Babel 6、Webpack 4)。sag-icons 的目标用户是现代前端,这条线是清楚的取舍。

main /module /types

"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
  • main:Node.js / 旧工具的入口
  • module:现代打包器的入口(webpack 4+ / rollup 优先看)
  • types:TypeScript 类型入口

sag-icons 这三个字段都指向同一个文件——./dist/index.js。因为 sag-icons 只发 ESM 一份产物,没有 CJS 兜底。

对应字段:package.json:6-8

exports —— 这是关键

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "import": "./dist/index.js"
  }
}

exports 是 Node.js 12+ 引入的精确子路径控制机制。它比 main / module 更严格:

  • 用户 import "@jujiuyey/sag-icons" 走 "." 块
  • "types" 条件 → TS 类型
  • "import" 条件 → ESM 产物

为什么不写 "require"?因为 sag-icons 只发 ESM。强写 require 反而会让 Node.js CJS 用户报错,提示明确。

现在 sag-icons 的 exports 只暴露了根路径。但理论上你可以加子路径:

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "import": "./dist/index.js"
  },
  "./css": "./dist/svg-icon.css",
  "./css/svg-icon.css": "./dist/svg-icon.css",
  "./data": "./dist/data/index.js"
}

让用户可以:

import "@jujiuyey/sag-icons";                  // 注册 Web Component
import "@jujiuyey/sag-icons/css/svg-icon.css"; // 单独引 CSS
import { workbench } from "@jujiuyey/sag-icons/data/icons/nav/workbench"; // 按需

这套子路径设计是 sag-icons 的精华,第 7 章会细讲。

files

"files": ["dist"]

npm publish 时只把 dist/ 目录发到 npm。src/、docs/、node_modules/ 这些都不发。

如果忘了写 files,npm 会发整个仓库——你的 README.md、开发脚本、文档站都会被发出去,包体积膨胀,也泄露内部细节。

对应字段:package.json:15-17

keywords /license

"keywords": ["icon", "svg", "web-components", ...],
"license": "MIT"

keywords 影响 npm 搜索排序。icon / svg 是必须的,web-components / custom-elements / framework-agnostic 是 sag-icons 的差异化卖点。

license: MIT 是事实标准——允许商业用、修改、分发。


4.2 TypeScript 双层配置

sag-icons 有两个 tsconfig:

  • tsconfig.json —— 给 IDE 和 tsc --noEmit 用(开发期)
  • tsconfig.build.json —— 给 pnpm build:lib 用(产物生成)

开发期:tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,                // ← 关键
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

注意 noEmit: true——开发期不产生任何文件。只做类型检查(pnpm typecheck)。

另外 allowImportingTsExtensions: true 允许 import "./foo.ts"。这在源码里很方便。

构建期:tsconfig.build.json

tsconfig.build.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "declaration": true,           // ← 关键:生成 .d.ts
    "declarationDir": "./dist",
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": [
    "src/data",
    "src/css",
    "src/web-components",
    "src/index.ts"
  ],
  "exclude": ["node_modules", "dist", "src/docs-components"]
}

关键差别:

  • declaration: true:生成 .d.ts 类型声明文件
  • outDir: "./dist":产物输出到 dist/
  • rootDir: "./src":源码根目录
  • include:精确指定要打包的文件——排除了 src/docs-components/(文档站用的演示组件,不该发到 npm)
  • exclude:排除测试、文档组件

为什么要两层

把 tsconfig.json 和 tsconfig.build.json 分开,是因为:

  1. 开发期需要更宽松:允许 import "./foo.ts"、不开 declaration、包含所有文件
  2. 构建期需要更严格:精确定义发包范围、生成类型、产物结构清晰

把这两件事混在一个 tsconfig 里会两头不讨好。sag-icons 用两层配置解决这个问题。

对应文件:tsconfig.json、tsconfig.build.json


4.3 选 Vite 库模式

sag-icons 用 Vite 而不是 tsup / rollup / unbuild。理由:

为什么是 Vite

  • 配置简单:build.lib 一段配置就够
  • 现代:默认 ESM、tree-shaking 好
  • Sourcemap 免费:sourcemap: true 一行
  • 生态熟悉:React/Vue 用户都会 Vite

vite.config.ts

vite.config.ts
import { defineConfig } from "vite";
import { resolve } from "path";

export default defineConfig({
  server: {
    open: "/preview.html",
  },
  build: {
    emptyOutDir: false,
    lib: {
      entry: resolve(__dirname, "src/index.ts"),
      name: "SagIcons",
      formats: ["es"],
      fileName: () => "index.js",
    },
    sourcemap: true,
    minify: false,
  },
});

几个关键点:

  • emptyOutDir: false:不清理 dist/,因为 tsc 已经先把 .d.ts 放那了。Vite 的 JS 产物会和 .d.ts 共存。
  • lib.entry:Vite 从 src/index.ts 开始打包
  • formats: ["es"]:只发 ESM
  • fileName: () => "index.js":固定文件名(不区分 hash),方便 package.json 引用
  • minify: false:保留可读性。图标库不需要极致压缩,gzip 已经够小

对应文件:vite.config.ts

为什么不选 tsup

tsup 也是 esbuild 包装的好选择,更适合纯库。但 sag-icons 同时跑 tsc(生成类型)和 vite build(打包 JS),这套流水线已经够稳定了。

要不要简化成 tsup 一把梭?你可以试:

tsup.config.ts
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm"],
  dts: true,
  sourcemap: true,
  clean: false,  // 同 Vite 一样,不清空 dist
});

sag-icons 没切是因为切换要重新跑 pnpm build:lib 验证,稳定优先。


4.4 CSS 作为字符串导出

sag-icons 把 CSS 写成 .ts 文件导出字符串:

src/css/svg-icon.ts
export const svgIconCss = `:root {
  --sag-icon-blue: #3b82f6;
  ...
}`;

而不是传统的 .css 文件。这是有意为之:

优势

  • 运行时注入:src/index.ts 里 import { svgIconCss },然后塞 <style> 到 head
  • 去重保证:用户 import "@jujiuyey/sag-icons" 多次也只注入一次(看 injectStyles() 函数)
  • 不需要单独的 CSS 引入:用户只 import 一次主入口就完事
  • 用户可选:不想自动注入的,可以 import "@jujiuyey/sag-icons/css/svg-icon.css" 拿原始 CSS

劣势

  • 不能静态分析:用户用工具(如 purgecss)扫 CSS 时看不到这部分样式
  • 不能用 PostCSS 处理:自动注入版本绕过了构建期 CSS 处理

sag-icons 的 CSS 很简单(一个 :root 变量 + 几个选择器),PostCSS 处理的收益不大。所以选了运行时字符串方案。

同时提供.css 文件

为了兼容传统 CSS 用户,sag-icons 还提供原始 CSS 入口:

// 假设的 package.json exports
"exports": {
  ".": { "...": "..." },
  "./css": "./dist/svg-icon.css",
  "./css/svg-icon.css": "./dist/svg-icon.css"
}

这里 .css 文件需要 Vite 单独处理——但 sag-icons 现在的 vite.config.ts 只配置了 lib.entry,没有处理 CSS。

这是 sag-icons 当前的一个小坑:用户 import "@jujiuyey/sag-icons/css/svg-icon.css" 会找不到文件,因为 vite build 没把 CSS 拷到 dist。

教程的第 8 章会讲怎么补这个。


4.5build:lib 双步流水线

pnpm build:lib
# = tsc -p tsconfig.build.json && vite build

这两步各管一件事:

第一步:tsc 生成类型

tsc -p tsconfig.build.json

跑 tsconfig.build.json,产出:

dist/ index.js # 但这是 .ts 的简单编译产物,没 bundle index.d.ts data/ icons/.../*.d.ts index.d.ts css/ svg-icon.d.ts svg-icon.js web-components/ register.d.ts register.js sag-icon.d.ts sag-icon.js

这一步只产类型和模块化 JS,不做 tree-shaking 或打包。慢但稳。

第二步:vite build 产打包后的 JS

vite build

跑 vite.config.ts,清掉 dist/ 里 tsc 产的多文件 JS,重新打成一个 bundle:

dist/ index.js # vite 打包的 bundle index.js.map index.d.ts # 保留 tsc 的类型 ...

emptyOutDir: false 让 Vite 不清空 dist,所以 .d.ts 还在。Vite 只覆盖 .js 文件。

这种"双流水线"配置是 Vite 库模式的常见 pattern:tsc 产类型,vite 产 JS。


4.6pnpm dev 是什么

sag-icons 有个反直觉的地方:pnpm dev 不是开发图标库——是跑文档站:

"scripts": {
  "dev": "rspress dev",
  "build": "rspress build"
}

sag-icons 本身的开发流程其实很简单:

  1. 改 src/data/icons/*.ts 加新图标
  2. 改 src/css/svg-icon.ts 调样式
  3. 改 src/web-components/sag-icon.ts 调组件逻辑
  4. pnpm typecheck 验类型
  5. pnpm build:lib 验产物
  6. pnpm dev 跑文档站看效果

文档站 (rspress dev) 是给用户看的,不是给自己开发用的。

对应文件:package.json:19-22


4.7 这一章的产出

到这里你应该能:

  • 解释 sag-icons 的 package.json 每个字段
  • 区分 tsconfig.json 和 tsconfig.build.json 的用途
  • 解释 Vite 库模式的关键配置
  • 跑通 pnpm build:lib 整个流水线

下一章进入 sag-icons 的核心——Web Component。这是为什么 sag-icons 一个包能跨 React / Vue / 原生 HTML 的关键。


对应文件

内容路径
package.jsonpackage.json
开发期 tsconfigtsconfig.json
构建期 tsconfigtsconfig.build.json
Vite 配置vite.config.ts
CSS 字符串src/css/svg-icon.ts