unplugin-derive
v0.6.1
Published
Generate derived artifacts from your project files with a flexible, unplugin-based abstraction.
Readme
unplugin-derive
基于 unplugin 的通用派生引擎:
- 监听
watch文件变化 - 触发
derive(event)(full/patch) - 根据返回的
files写入或删除目标文件
安装
pnpm add -D unplugin-derive用法(Vite)
import { defineConfig } from 'vite'
import Derive from 'unplugin-derive/vite'
export default defineConfig({
plugins: [
Derive({
watch: ['src/api/**/*.js'],
load: 'text',
async derive(event) {
const count = event.changes.length
const content = `// generated from ${event.type}, files: ${count}\n`
return {
files: [{ path: 'src/generated.txt', content }]
}
},
verbose: true
})
]
})其它构建工具(折叠)
Rollup
import Derive from 'unplugin-derive/rollup'
export default {
plugins: [
Derive({
watch: ['src/api/**/*.js'],
async derive() {
return {
files: [{ path: 'src/generated.txt', content: 'from rollup\n' }]
}
}
})
]
}Webpack
const Derive = require('unplugin-derive/webpack').default
module.exports = {
plugins: [
Derive({
watch: ['src/api/**/*.js'],
async derive() {
return {
files: [{ path: 'src/generated.txt', content: 'from webpack\n' }]
}
}
})
]
}esbuild
import { build } from 'esbuild'
import Derive from 'unplugin-derive/esbuild'
await build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/index.js',
plugins: [
Derive({
watch: ['src/api/**/*.js'],
async derive() {
return {
files: [{ path: 'src/generated.txt', content: 'from esbuild\n' }]
}
}
})
]
})配置总览
- 通用配置:
root、verbose - 监听触发:
watch、deriveWhen - 派生输出:
derive - 内容加载:
load - 输出加工:
banner - 后续处理:
gitignore
执行流程
一次 derive 任务的执行顺序如下:
- 收集变更(
full扫描watch,patch使用传入变更并做路径归一化) - 依次执行
load,为每个 change 补充content/loader(可选) - 调用
derive(event),拿到要写入/删除的files - 按配置维护
.gitignore(如果启用,基于derive返回的 files 计算) - 最终执行文件写入/删除(包含 banner 合并与渲染;在 emit 阶段会跳过越界路径与命中
watch的输出)
按执行顺序的配置详解
0) root 与 verbose(通用项)
root: 工程根目录(默认process.cwd())verbose: 输出运行日志(默认false)
1) watch 与 deriveWhen(何时触发)
watch: 监听文件 glob(相对root)- 支持否定模式:
!pattern(如['src/**/*.ts', '!src/**/*.test.ts']) deriveWhen.buildStart:full|none(默认full)deriveWhen.watchChange:patch|full|none(默认patch)- 当
watchChange: "full"时,仅在变更路径命中watch时触发 full
2) load(如何加载内容)
load 支持 5 种常见配置形态:
单个内置加载器
对所有命中的文件统一使用一个内置加载器('_text' | '_json' | '_buffer' | '_import')。
Derive({
watch: ['src/**/*.txt'],
load: '_text',
async derive(event) {
return {
files: [{ path: 'src/generated.txt', content: String(event.changes[0]?.content ?? '') }]
}
}
})单个自定义加载器
直接使用一个自定义加载器函数:(path) => ({ content }) | undefined。path 为相对 root 的路径。
Derive({
watch: ['src/**/*.md'],
load(file) {
if (!file.endsWith('.md')) return undefined
return { content: '# virtual markdown' }
},
async derive(event) {
return {
files: [{ path: 'src/generated.txt', content: String(event.changes[0]?.content ?? '') }]
}
}
})组合加载器
使用数组按顺序 fallback,命中即停止。数组项可混用内置与自定义加载器。
Derive({
watch: ['src/api/**/*'],
load: ['_json', '_text', file => ({ content: { fallback: file } })],
async derive(event) {
return {
files: [{ path: 'src/generated.txt', content: `event=${event.type}\n` }]
}
}
})动态单个加载器
使用函数按路径动态返回单个内置加载器。
Derive({
watch: ['src/**/*'],
load(file) {
if (file.endsWith('.json')) return '_json'
return '_text'
},
async derive() {
return { files: [] }
}
})动态组合加载器
使用函数按路径动态返回数组链。
Derive({
watch: ['src/**/*'],
load(file) {
if (file.endsWith('.json')) return ['_json', '_text']
return ['_import', '_text']
},
async derive() {
return { files: [] }
}
})补充说明:
- 兼容旧内置名:
'text' | 'json' | 'buffer' | 'import'在运行时仍可用,但推荐迁移到下划线写法。 - 不支持
load: () => () => ({ content })这类“返回函数”的嵌套形式。 change.loader为可选字段:内置 loader 会自动提供;自定义 loader 如需该信息,请在返回值中自行带上loader。
3) derive(如何产出文件)
derive 是核心回调,签名为 derive(event: DeriveEvent),返回 DeriveResult:
- 通过
event.changes读取本次输入 - 返回
files指定要写入或删除的目标文件 - 输出路径会在内部做安全过滤(越界路径、命中
watch的输出会被跳过)
事件和返回值
DeriveEventtype: "full" | "patch"changes: Array<{ type, path, content?, loader? }>
DeriveResultfiles: Array<{ path, content, banner? } | { path, type: "delete" }>banner?: DeriveBanner
4) banner(输出加工,可选)
- 覆盖顺序:
DerivePluginOptions.banner->DeriveResult.banner->DeriveResultFile.banner(后者覆盖前者) false也遵循同样规则,表示该层显式禁用data合并是浅合并:后者覆盖前者的同名 key(嵌套对象不会做深合并)style可选值:line-slash/line-hash/block-star/block-jsdoc
从简单到复杂,推荐这样使用:
- 只用默认模板(最省事)
- 只要最终合并后的
banner.data.author存在,就会渲染内置默认模板;否则不输出 banner。
Derive({
watch: ['src/**/*.ts'],
banner: {
data: {
author: 'team-a',
source: 'src/**/*.ts',
overview: {
description: 'generated stats',
items: ['files: 12', 'methods: 38']
}
}
},
async derive() {
return {
files: [{ path: 'src/generated.ts', content: 'export const x = 1\n' }]
}
}
})- 上层
template统一渲染(推荐)
设计目的:derive 可以按“文件维度”返回 banner.data,由上层的 template 统一渲染出一致的 banner 样式。
- 渲染优先级:
formatter>template> 默认模板 template的渲染作用域是{ data }(data-only scope):只能通过<%= data.xxx %>读取(例如<%= data.author %>、<%= data.source %>)
Derive({
watch: ['src/**/*.ts'],
banner: {
// 统一模板
template: 'author=<%= data.author %>, source=<%= data.source %>',
// 全局默认 data(可被 result/file 覆盖)
data: { author: 'team-a' }
},
async derive() {
return {
banner: {
// 本次任务维度补充/覆盖
data: { source: 'src/**/*.ts' }
},
files: [
{
path: 'src/generated.ts',
content: 'export const x = 1\n',
banner: {
// 文件维度补充/覆盖(例如把每个文件的 source 精确到来源文件)
data: { source: 'src/foo.ts' }
}
}
]
}
}
})- 需要
path/content/style时,用formatter(高级)
如果你确实需要读取输出文件路径、内容或 style(例如把路径写进 banner,或根据内容决定是否输出),建议直接提供 formatter 函数。
formatter(context)会收到{ path, content, data, style }context.path是相对root的输出路径(对外不暴露绝对路径)
Derive({
watch: ['src/**/*.ts'],
banner: {
style: 'line-slash',
formatter: ({ path, data }) => `generated=${path}; author=${data.author ?? ''}`,
data: { author: 'team-a' }
},
async derive() {
return {
files: [{ path: 'src/generated.ts', content: 'export const x = 1\n' }]
}
}
})5) gitignore(输出后处理,可选)
true: 将本次生成的可写入文件(即带content的 file,且路径位于root内)写入.gitignorestring/string[]: 直接作为.gitignore条目写入(file) => boolean: 按文件相对路径过滤后写入
其它
- 同一时刻只会执行一个
derive - 运行中收到
patch会合并排队 - 运行中收到
full会清空未开始的patch,并在当前任务结束后优先执行full
示例
- 项目特定的 API 解析和
types.d.ts渲染逻辑已放在examples/webpack-dts
测试
测试相关说明已迁移到 TESTING.md。
