dtc 是一个运行在 QuickJS 上的模板生成工具。它会读取一个 JSON 配置文件,执行入口数据脚本构建最终全局对象,筛选匹配模板的数据对象,再使用 EJS 渲染并输出目标文件。
dtc 适合用来做“用一份结构化数据,批量生成文本文件”的场景,例如:
- 根据模块配置生成代码文件
- 根据对象清单生成配置文件
- 根据一组业务定义生成文档、脚本或清单文件
它的核心思路是把“数据准备”和“模板渲染”拆开:
- 用
data/*.js组织和加工数据 - 用
tpl/*.tpl编写输出模板 - 让
dtc自动匹配数据对象和模板,生成最终文件
如果你不想在模板里塞很多数据处理逻辑,而是想先把数据整理干净,再稳定地产出目标文件,这个工具就比较合适。
假设你想根据一份模块定义,生成一个文本清单文件。
配置文件 dtc.json:
{
"data": "data/root.js",
"tpl": [
{
"files": ["tpl/*.tpl"],
"out": "out/modules.txt"
}
]
}数据文件 data/root.js:
export default {
modules: {
user: {
enable: true,
match: "module.tpl",
title: "User Module"
},
order: {
enable: true,
match: "module.tpl",
title: "Order Module"
}
}
};模板文件 tpl/module.tpl:
MODULE:<%= item.name %>|TITLE:<%= item.title %>运行后会生成 out/modules.txt:
MODULE:user|TITLE:User Module
MODULE:order|TITLE:Order Module
也就是说,你只需要:
- 在数据脚本里准备对象
- 给要参与渲染的对象加上
enable: true和match - 写一个模板
dtc 就会自动把命中的对象逐个渲染到目标输出文件里。
- 纯 JavaScript 实现,运行时为 QuickJS
- 使用
qjs:std、qjs:os访问 QuickJS 内建能力 - 支持
include()、remove()、replace()、update()、updateRoot()和get()构建全局对象 - 支持模板文件通配符搜索
- 支持对象
match通配符匹配模板文件名 - 自动为普通对象补充
name,并在模板渲染时提供parent - 支持打包为 Linux / Windows 可执行文件
- 当前版本由
src/version.js统一管理 - 如果要修改版本号,只需要改这一个文件
- CLI 可通过以下命令查看版本:
build/dtc.run --version或:
bin/qjs-linux-x86_64 src/index.js --version- 全部业务代码使用 JavaScript 实现
- 运行时是 QuickJS,不能依赖 Node.js API,也不能依赖浏览器 API
- 模板引擎固定为 EJS,当前通过
src/lib/ejs/ejs-wrapper.js适配到 QuickJS - 构建链路固定为
esbuild -> qjs -c -> 可执行文件 - 路径处理同时兼容 Linux 和 Windows,包括相对路径、绝对路径和分隔符差异
当前主流程已经可用,已经实现:
- 配置读取与校验
- 数据脚本执行、
include()、remove() - 全局对象深合并与类型冲突报错
name元信息补充与渲染期parent- 模板发现、
match匹配与 EJS 渲染 - 输出写盘
- 打包为
build/bundle.js、build/dtc.run、build/dtc.exe - 多组集成测试
.
├── bin/ QuickJS 可执行文件
├── build/ 构建产物
├── doc/ 设计文档与规格文档
├── src/
│ ├── app/ CLI、配置、数据、模板与调试等业务代码
│ ├── lib/ QuickJS 运行时适配、EJS 包装与公共工具
│ └── index.js CLI 入口
├── test/
│ ├── case-basic/ 基础成功用例
│ ├── case-enable-filter/ enable=true 过滤用例
│ ├── case-empty-output/ 无匹配输出用例
│ ├── case-get-missing/ get() 读取缺失路径报错用例
│ ├── case-type-mismatch/ 合并类型冲突用例
│ ├── case-circular/ 循环 include 用例
│ ├── case-wildcard/ 通配符匹配用例
│ └── test.js 测试入口
└── pbuild.sh 构建脚本
- 读取
dtc.json - 执行
data指定的入口脚本 - 在执行期间处理
include()、remove()、replace()、update()、updateRoot()和get() - 合并所有存在默认导出的对象,得到最终全局对象
- 为普通对象补充
name - 在模板渲染时临时提供
parent - 按
tpl[].files搜索模板文件 - 遍历最终全局对象中带
match的普通对象 - 用 EJS 渲染匹配到的模板
- 将结果写入
tpl[].out
项目的正式规格和实现说明都在 doc/ 下:
doc/spec-vision.md:整体目标、处理流程、边界和输出规则doc/spec-config.md:配置文件结构和路径规则doc/spec-data-model.md:入口脚本、include()、remove()、replace()、update()、updateRoot()和对象合并语义doc/spec-templates.md:模板发现、match规则和输出聚合doc/spec-rendering.md:EJS 渲染上下文和 QuickJS 约束doc/architecture-build.md:源码模块划分和构建链路doc/implementation-status.md:当前实现覆盖情况
- 已提供 QuickJS 二进制:
bin/qjs-linux-x86_64bin/qjs-windows-x86_64.exe
- 需要本机安装
esbuild
如果尚未安装 esbuild:
npm install -g esbuild最小示例:
{
"data": "data/root.js",
"debugDataOut": "debug/global-data.json",
"debugMatchOut": "debug/match-data.json",
"ejs": {
"openDelimiter": "[",
"closeDelimiter": "]"
},
"tpl": [
{
"files": [
"tpl/*.tpl"
],
"out": "out/generated.txt"
}
]
}字段说明:
data:入口数据脚本,相对于配置文件所在目录debugDataOut:可选,全局对象调试输出文件;为空或未配置则不输出debugMatchOut:可选,模板与命中对象调试输出文件;为空或未配置则不输出ejs.openDelimiter:可选,EJS 左侧分隔符外层字符,默认是<ejs.closeDelimiter:可选,EJS 右侧分隔符外层字符,默认是>tpl[].files:模板文件模式列表,支持*、?、**tpl[].out:输出文件路径,相对于配置文件所在目录
调试输出说明:
debugDataOut输出的是补充name之前的全局对象 JSON 快照debugMatchOut输出的是模板路径和命中对象数据列表- 模板调试对象默认不写入
parent
自定义 EJS 分隔符说明:
- 如果未配置
ejs.openDelimiter/ejs.closeDelimiter,则继续使用默认标识<%和%> - 如果配置为
[和],则模板标识会变成[%和%]
示例:
{
"data": "data/root.js",
"ejs": {
"openDelimiter": "[",
"closeDelimiter": "]"
},
"tpl": [
{
"files": ["tpl/*.tpl"],
"out": "out/generated.txt"
}
]
}对应模板可以写成:
[%= item.name %]|[%= item.title %]数据脚本必须是 ES Module。它既可以通过 export default 返回一个对象参与合并,也可以不导出默认对象、只通过脚本副作用直接修改当前全局对象。
常用函数可以这样理解:
| 函数 | 适合场景 | 示例 |
|---|---|---|
include() |
拆分数据脚本,按文件组织数据 | include("sub.js") |
remove() |
删除不需要的字段 | remove("modules.obsolete") |
replace() |
直接覆盖某个路径的值 | replace("docs.item.note", "created") |
update(path, object) |
保留原对象其他字段,只补充或修改其中一部分 | update("meta", { version: "1.0.0" }) |
update(path, scalar) |
更新一个已存在且类型匹配的属性值 | update("modules.detail.title", "new-title") |
updateRoot() |
一次更新多个顶层字段 | updateRoot({ flags: { enabled: true } }) |
get() |
读取当前全局对象中的值,供后续计算使用 | const version = get("meta.version") |
示例:
include("sub.js");
include("patch.js");
remove("modules.obsolete");
const detailTitle = get("modules.detail.title");
const secondName = get("lookup.items.1.name");
update("modules.detail.title", "detail-updated-by-root");
update("meta", {
extra: "added-by-update",
detailTitle,
secondName
});
replace("docs.item.note", "created-before-merge");
updateRoot({
flags: {
fromRootPatch: true
}
});
export default {
meta: {
version: "1.0.0"
},
modules: {
match: "main.tpl",
title: "main-from-root"
}
};规则摘要:
include("sub.js"):按当前脚本所在目录解析相对路径remove("a.b.c"):从当前全局对象删除点分路径replace("a.b.c", value):直接替换路径上的值,必要时自动创建缺失路径update("a.b.c", patchObject):如果传入普通对象,则把对象补丁深合并到目标对象上,必要时自动创建缺失路径update("a.b.c", value):如果传入非对象值,则只允许更新已存在且类型匹配的字段,否则报错updateRoot(patchObject):把普通对象补丁直接深合并到全局根对象get("a.b.c"):读取当前全局对象中的值;路径不存在时报错,支持数组下标如items.1.nameget()返回对象或数组时,可直接修改返回值,从而以副作用方式更新全局对象- 数据脚本没有
export default时,不会额外合并对象,但脚本副作用仍然生效 - 只有
enable === true且match命中的对象才会参与模板渲染 - 合并时类型不匹配会报错
- 数组中的对象不会参与模板匹配遍历
每次模板渲染时可使用这些变量:
{
item,
parent,
root,
template,
output
}示例:
<%= item.name %>|<%= parent ? parent.name : "root" %>|<%= root.meta.version %>使用源码入口运行:
bin/qjs-linux-x86_64 src/index.js path/to/dtc.json查看版本:
bin/qjs-linux-x86_64 src/index.js --version使用打包后的 bundle 运行:
bin/qjs-linux-x86_64 build/bundle.js path/to/dtc.json执行:
./pbuild.sh构建后会生成:
build/bundle.jsbuild/dtc.runbuild/dtc.exe
运行编译后的 Linux 可执行文件:
build/dtc.run path/to/dtc.json查看编译产物版本:
build/dtc.run --version运行全部测试:
bin/qjs-linux-x86_64 --module test/test.js当前测试覆盖:
- 基础渲染流程
- 版本号来源
- 调试输出文件
include()/remove()/replace()/update()/updateRoot()/get()- 无
default export模块的副作用更新 enable === true过滤逻辑get()读取缺失路径报错name与渲染期parent- 空输出行为
- 通配符模板匹配
- 合并类型冲突
- 循环
include()
更详细的规格和设计说明见 doc/ 下各文档。