使用 SvelteKit 打包与发布组件库

SvelteKit 既能构建应用,也能通过 @sveltejs/package 构建组件库。npx sv create 提供了自动配置选项。

构建应用时,src/routes 面向用户,src/lib 是内部库。组件库的目录结构与应用相同,但面向使用者的是 src/lib,发布使用根目录的 package.json。src/routes 可以是随库提供的文档、演示站点,也可以只是开发时的试验场。

运行 svelte-package 后,会将 src/lib 内容处理到可配置的输出目录,默认 dist,其中包含:

  • src/lib 内全部文件。Svelte 组件经过预处理,TypeScript 转为 JavaScript。
  • 为 Svelte、JavaScript 和 TypeScript 生成的 .d.ts 类型声明。需要安装 TypeScript 4.0.0 或更新版本。声明放在实现文件旁边,手写声明原样复制。可以禁用生成,但强烈不建议,因为使用者可能依赖 TypeScript 类型。

@sveltejs/package 第 1 版会生成 package.json,现在改为使用项目现有文件并验证其正确性。仍在使用第 1 版时,请参阅原文链接的迁移 PR。

package.json 的结构

公开发布库时,package.json 更加重要。它配置入口、发布文件与依赖。下面介绍主要字段。

name

包名用于安装,并显示在 https://npmjs.com/package/<name>:

{
	"name": "your-library"
}

license

每个包都应有 license 字段,让使用者了解授权方式。MIT 是常见且宽松的许可,允许分发与复用,但不提供担保:

{
	"license": "MIT"
}

包中还应包含 LICENSE 文件。

files

指定 npm 打包上传的文件,应包含输出目录,默认 dist。package.json、README 和 LICENSE 始终会包含,无需额外列出:

{
	"files": ["dist"]
}

可在 .npmignore 排除单元测试、仅供 src/routes 导入的模块等不必要文件,使包更小、安装更快。

exports

exports 指定包入口。通过 npx sv create 建库时,默认只导出包根路径:

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

这告诉打包器和工具,库只有一个根入口,应从这里导入全部内容:

import { Something } from 'your-library';

types 与 svelte 是导出条件,指导工具解析:

  • TypeScript 识别 types 并加载声明文件。不发布类型声明时,应省略它。
  • 支持 Svelte 的工具识别 svelte,知道这是组件库。如果库不导出 Svelte 组件,且也适用于非 Svelte 项目,例如某些 store 库,可以改为 default。

旧版 @sveltejs/package 还会导出 package.json,现在模板不再包含,因为工具已能处理未显式导出的 package.json。

可以自定义 exports,提供更多入口。例如,不通过 src/lib/index.js 重新导出,而是直接暴露 src/lib/Foo.svelte:

{
	"exports": {
		"./Foo.svelte": {
			"types": "./dist/Foo.svelte.d.ts",
			"svelte": "./dist/Foo.svelte"
		}
	}
}

使用者可以这样导入:

import Foo from 'your-library/Foo.svelte';

如果发布类型声明,需要额外注意,详见后面的 TypeScript 部分。

一般而言,exports 的键就是用户导入时使用的路径,值则是目标文件路径,或包含这些路径的条件映射。

svelte

这是帮助工具识别 Svelte 组件库的旧字段。使用 svelte 导出条件后已不再必需,但为了兼容尚不认识条件导出的旧工具,保留它仍有价值。它应指向根入口:

{
	"svelte": "./dist/index.js"
}

sideEffects

打包器用 sideEffects 判断模块是否含有副作用。导入模块时,若发生可被其他脚本观察到的变化,例如修改全局变量或内置对象原型,就属于副作用。由于可能影响应用其他部分,这些模块即使导出未被使用,也会保留在最终包中。尽量避免副作用是良好实践。

正确声明 sideEffects,可以让打包器更积极地执行 tree-shaking,删除未用导出,得到更小、更高效的包。不同打包器处理方式不同。Vite 虽不要求,但为兼容 webpack,建议将全部 CSS 声明为有副作用。新项目默认:

{
	"sideEffects": ["**/*.css"]
}

如果库中的脚本有副作用,务必更新字段。新项目默认把所有脚本视为无副作用,标错可能破坏功能。

可以用数组明确指定:

{
	"sideEffects": [
		"**/*.css",
		"./dist/sideEffectfulFile.js"
	]
}

这样只有列出的文件会被视为有副作用。

TypeScript

即使自己不用 TypeScript,也应提供类型声明,让使用者获得正确的智能提示。@sveltejs/package 自动处理大部分生成工作,默认在打包时为 JavaScript、TypeScript 和 Svelte 生成声明。只需确保 exports 中的 types 指向正确文件。通过 CLI 创建项目时,根导出会自动配置。

如果有根入口以外的导出,例如 your-library/foo,就需要格外注意。TypeScript 默认不会按 { "./foo": { "types": "./dist/foo.d.ts", ... }} 解析,而是从库根目录寻找 foo.d.ts,即寻找 your-library/foo.d.ts,而不是 dist 下的文件。

有两种解决方案:

  1. 要求使用者在 tsconfig.json 或 jsconfig.json 中,将 moduleResolution 设为 bundler、node16 或 nodenext。bundler 从 TypeScript 5 开始提供,是推荐的未来选择。这样 TypeScript 会读取 exports 映射并正确解析。
  2. 利用 typesVersions 的路径映射。它原本根据 TypeScript 版本选择声明,也能用于建立子路径映射:
{
	"exports": {
		"./foo": {
			"types": "./dist/foo.d.ts",
			"svelte": "./dist/foo.js"
		}
	},
	"typesVersions": {
		">4.0": {
			"foo": ["./dist/foo.d.ts"]
		}
	}
}

>4.0 表示使用的 TypeScript 大于 4 时读取内部映射,实际中通常成立。内部映射说明 your-library/foo 的类型位于 ./dist/foo.d.ts,本质上复现了 exports 条件。也可以用 * 通配符批量声明,避免重复。

一旦使用 typesVersions,就必须通过它声明所有类型导入,包括根导入,后者使用 "index.d.ts": [..]。

最佳实践

除非库只面向 SvelteKit 项目,否则避免使用 $app/env 等专有模块。例如,可用 import { BROWSER } from 'esm-env' 替代 import { browser } from '$app/env'。也可将当前 URL、导航操作等作为属性传入,避免直接依赖 $app/state、$app/navigation。这种通用设计也更容易搭配测试、UI 演示等工具。

库内别名应使用子路径导入。打包时,svelte-package 会根据 package.json 的 imports,将 # 前缀导入改写为相对路径。

仔细判断变更是修复、新功能还是破坏性变化,并相应更新版本。从已有库的 exports 删除路径或导出条件,都应视为破坏性变更:

{
	"exports": {
		".": {
			"types": "./dist/index.d.ts",
// changing `svelte` to `default` is a breaking change:
			"svelte": "./dist/index.js"
			"default": "./dist/index.js"
		},
// removing this is a breaking change:
		"./foo": {
			"types": "./dist/foo.d.ts",
			"svelte": "./dist/foo.js",
			"default": "./dist/foo.js"
		},
// adding this is ok:
		"./bar": {
			"types": "./dist/bar.d.ts",
			"svelte": "./dist/bar.js",
			"default": "./dist/bar.js"
		}
	}
}

源映射

在 tsconfig.json 设置 "declarationMap": true,可以生成 .d.ts.map 声明映射,让 VS Code 等编辑器在“转到定义”时进入原始 .ts 或 .svelte 文件。

这也要求把源文件与 dist 一起发布,确保声明文件中的相对路径指向实际存在的文件。若库代码都在 CLI 推荐的 src/lib,只需将它加入 package.json 的 files:

{
	"files": [
		"dist",
		"!dist/**/*.test.*",
		"!dist/**/*.spec.*",
		"src/lib",
		"!src/lib/**/*.test.*",
		"!src/lib/**/*.spec.*"
	]
}

命令选项

svelte-package 支持:

  • -w / --watch:监视 src/lib 变化并重新构建。
  • -i / --input:包含全部包文件的输入目录,默认 src/lib。
  • -o / --output:处理后文件的输出目录,默认 dist。exports 应指向其中的文件,files 应包含该目录。
  • -p / --preserve-output:打包前不删除输出目录。默认 false,即先清空。
  • -t / --types:是否生成 .d.ts,默认 true。强烈建议启用,有助于生态中的库质量。
  • --tsconfig:指定 tsconfig 或 jsconfig 路径;未提供时,向工作区路径的上层查找最近的配置。

发布

发布生成的包:

npm publish

注意事项

所有相对文件导入都必须遵循 Node ESM 算法,写出完整文件名与扩展名。例如 src/lib/something/index.js:

import { something } from './something/index.js';

使用 TypeScript 时,导入 .ts 文件也应按同样方式书写,但扩展名写 .js,而不是 .ts。这是 TypeScript 的设计决定。将 moduleResolution 设为 NodeNext,有助于发现相关问题。

除 Svelte 文件会预处理、TypeScript 会转为 JavaScript 外,其他文件都原样复制。


原文:Packaging。作者/维护者:SvelteKit 文档贡献者。本文为原文的中文译文;代码保留原文内容。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容