构建期产物
模块可以附带一个目录,供消费方在构建期而非运行时使用——最有用的情形是一个供其他模块编译时依赖的包。Wippy 把它们称为产物(artifact):带有 meta.artifact.format 标记的普通 WAPP 文件系统资源。
这就是共享包如何抵达另一个仓库中的模块的方式。路径别名只能在一个仓库内解析;而产物随模块一同流转。
设计层解释了什么属于这样的包、什么不属于;本页讲的是运送它的机制。
声明产物
生产方声明一个普通的 fs.directory 并给它标记一个格式:
# src/_index.yaml
entries:
- name: package_fs
kind: fs.directory
meta:
comment: 消费方在构建期物化的 npm 包。
artifact:
format: node-package
directory: ./package
其他什么都不变:该资源像任何其他 fs.directory 一样被嵌入 WAPP——在 wippy.yaml 的 embed: 下列出它,或者给 wippy publish 和 wippy pack 传 --embed;未被嵌入的目录既不会被打包也不会被验证。已声明的产物会在模块发布和应用打包期间被验证,因此格式错误的产物会在发布时失败,而不是在消费方那里失败。
格式
格式适配器决定一个目录如何被验证、它具有什么标识,以及它落在哪里。Wippy 内置了一种:
| 格式 | 拥有的子树 | 验证 |
|---|---|---|
node-package |
npm/ |
package.json |
node-package 要求有 name 和一个语义化的 version,并且拒绝 preinstall、install、postinstall 和 prepare 生命周期脚本——被物化的包不得在安装时执行任何东西。它写入物化根目录下的 npm/<package name>。
格式必须在执行该工作的二进制文件中注册。宿主可以注册额外的格式;重复的名称和重叠的根目录会被拒绝。
物化
大多数时候你不需要运行任何东西。物化输出会在以下时机被自动协调:
- 全量和定向的
wippy install与wippy update - 冷启动
- 基于 Hub 的动态安装、更新和卸载
全量安装、更新、冷启动和运行时依赖协调是精确的:过期的输出会被清除。定向安装只叠加被选中的模块,并保留属于未被选中模块的输出。
本地模块替换走与打包资源相同的验证和物化生命周期,因此被替换模块的产物表现得与已发布的产物一样。
显式物化
对于需要在运行时介入之前拿到产物的构建步骤,CLI 直接暴露了它:
wippy artifacts materialize <pack.wapp> <namespace:name> [--root <directory>]
--root 默认为 .wippy。该资源必须声明 meta.artifact.format,且该格式必须在此 CLI 中注册。
要清楚这个命令故意不做什么:它不解析模块依赖,不修改 wippy.lock,不调用包管理器,也不参与运行时组合。它只是从一个 WAPP 中验证一个产物并把它写到磁盘上。
输出落在哪里
artifact.materialization_root 配置由应用拥有的输出根目录。它的默认值是依赖 vendor 目录的父目录。每种格式在它之下拥有一个互不重叠的子树,因此 node-package 的输出始终位于 <root>/npm/ 下。
物化是事务性的。内容先被验证并暂存,受管理的根目录在进程锁下被原子地交换,失败会随外围的注册表事务一起回滚,被中断的交换会在下一次运行时恢复。
完整示例:一个共享的前端包
一个唯一职责是发布包的生产方模块——它在运行时不提供任何服务:
# platform/ui-kit/src/_index.yaml
version: "1.0"
namespace: kickside.ui_kit
entries:
- name: package_fs
kind: fs.directory
meta:
artifact:
format: node-package
directory: ./package
消费方在安装依赖之前把它物化到自己的目录树中:
wippy artifacts materialize kickside-ui-kit-1.5.0.wapp \
kickside.ui_kit:package_fs --root ./.wippy
这会写入 ./.wippy/npm/@kickside/ui-kit。消费方用一个普通的 workspaces glob 把它捡起来,从那之后的解析就是普通的 node 解析:
{
"workspaces": ["./.wippy/npm/@*/*"]
}
npm install
从这个形态中有两点值得借鉴:
- 这个包是它自己的模块,而不是某个更大模块里的一个目录。 产物携带自己的
package.json版本,把它绑定到一个会因无关原因而变化的模块,会导致每当一方变动就必须发布另一方。 - 消费方把它当作普通依赖来解析。 一旦物化,就不存在 Wippy 特有的导入路径,这正是同一份源码既能在 monorepo 内构建、也能在其外构建的原因。
端到端:编写、开发循环、CI
编写生产方
对于包产物,通常没有什么可构建的——目录本身就是交付物。一个 CSS 词汇包只是一些文件加上一份清单:
platform/ui-kit/
├── src/_index.yaml # 把 package_fs 声明为产物
└── package/ # 将成为 npm 包的目录
├── package.json
├── kx-card.css
└── kx-state.css
{
"name": "@kickside/ui-kit",
"version": "1.5.0",
"type": "module",
"sideEffects": ["*.css"],
"exports": {
"./kx-card.css": "./kx-card.css",
"./kx-state.css": "./kx-state.css"
},
"files": ["kx-card.css", "kx-state.css", "package.json"]
}
sideEffects 对纯 CSS 包很重要:没有它,打包器可以自由地把被导入的样式表当作死代码丢弃。
包版本必须等于模块版本。 wippy publish 会验证这一点并拒绝不匹配,所以请同时提升两者。这也是给共享包自己的模块而不是把它嵌套在更大模块中的原因——否则宿主模块的每一次无关改动都会迫使该包发布新版本,反之亦然。
发布
# 只验证不发布
wippy publish --dry-run --version 1.5.0 --embed package_fs
# 发布
wippy publish --create --module-type library --module-visibility public --version 1.5.0 --embed package_fs
已声明的产物会作为发布的一部分被验证,因此不符合该格式规则的 package.json 会在这里被拒绝,而不是在消费方的构建中。
开发循环
每次编辑都发布不是开发循环。在本地打包生产方,并让消费方的物化步骤指向那个文件:
# 在生产方模块中
wippy pack /tmp/ui-kit-dev.wapp --embed package_fs
# 消费方从本地 pack 而不是已发布的 pack 物化
UI_KIT_WAPP=/tmp/ui-kit-dev.wapp make ui-kit MOD=workflows
让这个覆盖成为开发路径与 CI 之间唯一的差异——一个用来选择 pack 文件的环境变量,其下游的一切完全相同。物化方式与 CI 不同的开发循环就不再能预测 CI 的结果。
接入 make 和 CI
把物化步骤做成消费方构建的前置依赖,而不是靠人记得去运行的东西:
UI_KIT_WAPP ?=
build:
@case " $(UI_KIT_CONSUMERS) " in *" $(MOD) "*) $(MAKE) ui-kit MOD=$(MOD);; esac
cd $(call fe_dir,$(MOD)) && npm run build
这样 CI 就完全不需要任何针对产物的专门步骤:它运行同一个 make build,UI_KIT_WAPP 未设置,因此走的是抓取并物化的路径,针对的是在 build-inputs 中锁定的已发布版本。全新检出的代码不可能针对过期或缺失的包进行编译,而从未听说过产物的贡献者仍然能得到正确的构建。
你仍然需要自己动手做的部分
wippy artifacts materialize 刻意保持窄小,因此消费产物的构建目前需要自己把四个步骤粘合起来。知道是哪四个可以省去重新摸索:
1. 获取 .wapp。 该命令接受的是pack 文件路径,而不是模块引用,并且不解析依赖——所以必须有别的东西先把生产方抓下来。可行的模式是一个微型 Wippy 项目,它唯一的职责就是锁定并下载它:
# build-inputs/wippy.lock — 一个只为抓取而存在的项目
directories:
modules: .wippy
src: ./src
modules:
- name: kickside/ui-kit
version: 1.5.0
hash: be1eafd5…
( cd build-inputs && wippy install )
wapp=$(ls build-inputs/.wippy/vendor/kickside/ui-kit-*.wapp | grep -v sha256 | sort | tail -1)
在这里而不是在应用锁文件中锁定它,可以把构建期输入排除在运行时依赖图之外。
2. 为每个消费方物化一次,物化到消费方的包管理器能看到的根目录:
wippy artifacts materialize "$wapp" kickside.ui_kit:package_fs --root ./ui/.wippy
3. 接线消费方的 package.json。 物化写入文件;它不会编辑清单。只有当消费方同时声明了 workspace glob 和依赖时,npm 才会链接该包:
{
"workspaces": ["./.wippy/npm/@*/*"],
"dependencies": { "@kickside/ui-kit": "*" }
}
版本是 *,因为被物化的包自带版本。把这一步脚本化并使其幂等——如果接线缺失,构建会在很久之后以一个光秃秃的样式表 ENOENT 失败,这读起来像是文件缺失,而不是接线缺失。
4. 运行包管理器。 materialize 不会调用它,所以在第 3 步之后 npm install 需要你自己调用。
把它们合在一起,放进一个以消费方模块为参数的目标中:
ui-kit:
@set -e; \
( cd build-inputs && $(WIPPY) install ); \
wapp=$$(ls build-inputs/.wippy/vendor/kickside/ui-kit-*.wapp | grep -v sha256 | sort | tail -1); \
test -n "$$wapp" || { echo "no ui-kit .wapp; is the module published?"; exit 1; }; \
$(WIPPY) artifacts materialize "$$wapp" kickside.ui_kit:package_fs --root $(DIR)/.wippy; \
cd $(DIR) && node ../../scripts/wire-ui-kit.mjs && npm install --no-audit --no-fund
把整个目标做成消费方构建的前置依赖,这样全新检出的代码就不可能针对过期或缺失的包进行编译。
范围之外
产物有意不引入第二套解析器、包注册中心、归档格式、锁文件 schema、Hub API 或模块清单。仅构建期的依赖语义、再分发策略和宿主 ABI 验证是各自独立的问题,这里并不解决它们。