npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ice-web-components-dsl

v0.6.0

Published

A JSON-first DSL layer for AI agents to build forms with ice-web-components.

Readme

ice-web-components-dsl

JSON-first DSL for AI agents to build forms with ice-web-components.

ice-web-components 的表单是三层的:控件(ICETextField / ICESelect / ICEInputNumber…)

  • ICEFormItem(标签与错误排版)+ ICEForm(把控件与模型接起来)。每个字段要写三层构造, 而 name / label / control / rules 分散在两个对象里。

所以这个包不是「把 new ICEForm(...) 换个写法」。它补的是那三层不做、 而模型最容易写错的三件事:

| | 原生写法 | ice-web-components-dsl | | --- | --- | --- | | 输入 | 每个字段三层构造,name/label/control/rules 分散 | 一个扁平 fields[]{ name, type, label, ... } | | 约束 | new ICEInputNumber({ min: 18 })rules: [{ min: 18 }] 互不相干 | 写一次 min: 18,控件夹取与校验两处都有了 | | 出错时 | 控件画出来了但行为不对,只能自己猜 | 结构化诊断:类型不认识列出可用类型、依赖写错列出可用字段名 |

编译产物就是普通的 ICEPanel + ICEForm + ICEFormItem + 控件 —— 命中测试、键盘导航、 无障碍镜像、主题、序列化全部照旧。

1. 安装

npm install ice-web-components-dsl ice-web-components ice-render

2. 30 秒

{
  "schemaVersion": 1,
  "kind": "form",
  "title": "泵站参数确认",
  "description": "这三项确认后才会下发控制指令。",
  "fields": [
    { "name": "station", "type": "text", "label": "泵站名称", "required": true, "maxLength": 20 },
    { "name": "mode", "type": "select", "label": "运行模式", "required": true, "default": "auto",
      "options": [{ "value": "auto", "label": "自动" }, { "value": "manual", "label": "手动" }] },
    { "name": "flow", "type": "number", "label": "目标流量 (m³/h)", "required": true, "min": 0, "max": 5000, "step": 10 }
  ],
  "submitText": "确认下发"
}
import { renderFormDsl, validateFormDsl, compileFormDsl } from 'ice-web-components-dsl';

const { compiled, diagnostics } = renderFormDsl('canvas-id', dsl, {
  onSubmit(values) {
    console.log(values); // { station: '一号泵站', mode: 'auto', flow: 800 }
  },
});

// 或者只要诊断(validate 不抛异常,任何输入都能吃)
const result = validateFormDsl(dsl);
if (!result.valid) console.log(result.errors.map((e) => e.message).join('\n'));

// 或者只要组件树(自己挂到已有的 ICE 实例上)
const { container, form, model } = compileFormDsl(dsl);
ice.addChild(container);

浏览器直接用(UMD,注意顺序):

<canvas id="form" width="440" height="500"></canvas>
<script src="https://unpkg.com/ice-render/dist/index.umd.js"></script>
<script src="https://unpkg.com/ice-web-components/dist/index.umd.js"></script>
<script src="https://unpkg.com/ice-web-components-dsl/dist/index.umd.js"></script>
<script>
  ICEWEBDSL.renderFormDsl('form', { kind: 'form', fields: [{ name: 'a', type: 'text' }] });
</script>

3. 四个导出

| 导出 | 说明 | | --- | --- | | validateFormDsl(dsl) | 任何输入都不抛异常。返回 { valid, errors, warnings },每条诊断带 severity / code / message / path | | formatDiagnostics(result) | 把校验结果转成可读文本 | | compileFormDsl(dsl, opts?) | → { container, form, model, submitButton, fieldNames, getValues, setValues, reset, setWidth, submit, submitAsync, onSubmit, destroy }opts.width / opts.maxWidth 见 §8.1。校验不过抛 FormDslCompileError(带诊断) | | renderFormDsl(target, dsl, opts?) | 一步到位:建 ICE 实例 + 挂组件 + 接提交。返回 { ice, compiled, diagnostics, width, resize, setWidth, measureContentHeight, destroy }width 是表单最终宽度(已夹过 maxWidth),见 §8.1 |

4. 字段类型与它们接受的属性

20 个类型FORM_DSL_FIELD_TYPES)。加新类型的硬条件是"值能经 JSON 往返" —— 判据是运行时的,不是从文档看来的(见 §4.1)。

| type | 取值类型 | 该类型额外接受 | | --- | --- | --- | | text / textarea | string | allowClear showCount | | password | string | allowClear showCount showToggle | | number | number | step precision | | slider | number | step range | | checkbox / switch | boolean | — | | radio-group / checkbox-group | string / string[] | options direction(多选组还有 maxChecked) | | select | string(mode: multiple 时 string[]) | options modesingle/multiple/tagsshowSearch | | date | string(YYYY-MM-DD) | placement | | color | string(hex) | options(色板) | | rate | number | —(max = 满分几颗星) | | time | string(HH:mm:ss) | formatHH:mm:ss / HH:mm) | | segmented | string | options block | | autocomplete | string | options(候选) | | cascader | string(最深一层的叶子) | options(带 childrenseparator | | tree-select | string(mode: multiple 时 string[]) | options(带 childrenmode showSearch | | transfer | string[] | options(候选池) | | date-range | [起, 止] | —(required = 两头都在) |

所有类型都还接受:name type label placeholder default required min max minLength maxLength pattern message rules dependencies width props

白名单之外的键会被忽略,并给出警告 —— 警告里会列出这个类型接受哪些键。这就是"字段表封闭"的好处: 模型不会静默地写一个不生效的属性。

options 两种写法都收[{ "value": "a", "label": "甲" }] 或直接 ["a", "b"]。 库里不同控件要的形状不一样(colors: string[] / options: string[] / {value,label}[] / nodes: {key,label} / dataSource: {key,title}),那些差别由编译期归一化 —— 否则就是在收"模型记不住哪个是哪个"的税。

4.1 判据是运行时的:值能不能经 JSON 往返

ice-web-components 有 84 个 UI 组件,但不是每个都能当字段。判据不是"有没有 value 构造参数", 也不是"有没有 getFormValue"(那是 ICEWidget 基类给的,人人都有),而是 setFormValue(v) 之后 getFormValue() 还回不还得出同一个东西

| 组件 | 运行时表现 | 结论 | | --- | --- | --- | | ICEColorPicker | 3 → "3"["a","b"] → "a,b"(强制转字符串) | ✅ 真实现 | | ICEDateRangePicker | 只认两头齐全的元组,其余回落成 [null,null] | ✅ 真实现 | | ICETransfer | 只认 string[],其余回落成 [](按数据源过滤) | ✅ 真实现 | | ICERadioButton | getFormValue() 返回布尔(只表示自己勾没勾,互斥要调用方维护) | ❌ 假单选 | | ICEUpload | setFormValue 照收不误(基类默认),组件本身不参与取值 | ❌ 没实现 |

证据在 tests/field-values.test.ts(构造出来真调一遍),tools/probe-field-values.mjs 是同一件事的交互版。ICERadioButton / ICEUpload 因此不在类型表里 —— 不是漏了,是判过不能用。

5. 一处声明、两处生效

{
  "name": "age",
  "type": "number",
  "label": "年龄",
  "min": 18,
  "max": 65
}

min / max同时

  1. 传给 ICEInputNumber,约束步进按钮的夹取范围;
  2. 生成 ICEFormRule,拦住用户手输的越界值。

原生路径这两件事是分开的:new ICEInputNumber({ min: 18 }) 只影响步进按钮, 手输 10 不会报错,除非你再写一条 rules: [{ min: 18 }]。模型只会写其中一个。

文本类字段的 maxLength 同理:既限制输入长度,也生成校验规则。

6. 规则:shorthand 与 rules

| shorthand | 说明 | | --- | --- | | required | 必填;null / 空串 / 空数组 / false 都算缺失 | | min / max | 数值上下界 | | minLength / maxLength | 字符串或数组长度 | | pattern | 正则字符串(JSON 里写不出正则字面量,编译时转 RegExp) | | message | 自定义错误文案 |

shorthand 会合成一条规则(原生 ICEFormRule 本来就允许一条规则带多个约束)。 需要多条独立规则时用 rules,按数组顺序接在 shorthand 之后:

{
  "name": "code",
  "type": "text",
  "required": true,
  "rules": [{ "minLength": 4 }, { "pattern": "^[A-Z0-9-]+$", "message": "只能是大写字母、数字与连字符" }]
}

validator / asyncValidator 是函数,JSON 表达不了,不收(写了会被 unknown-rule-key 告警拦下)。

7. 跨字段依赖

{
  "name": "confirm",
  "type": "password",
  "label": "确认密码",
  "required": true,
  "dependencies": ["password"]
}

被依赖字段一变,本字段立刻重算 —— 不用等用户再动本字段一次。 依赖名写错会被拦下,诊断里列出可用字段名(含前向引用正确、后向引用也正确)。

8. 布局:为什么没有坐标

DSL 里不可表达 left / top。字段纵向堆叠交给 ICEForm 的箱式布局, 标签在上还是左侧由顶层 layout 决定。

理由很实际:ice-web-components 有 84 个组件,一旦放开坐标,模型就会产出互相重叠的控件 —— "让它自己摆"是唯一可行的口径。

宿主负责的是画布多大renderFormDslresize(w, h),内部走引擎的 ICE.fitCanvasToDisplaySize()),而不是每个控件摆在哪。

8.1 宽度:宿主说了算,但必须显式告诉它

宽度不在 DSL 里定,因为表单多宽取决于它被放在哪儿,而 DSL 不知道这件事。 宿主通过 width 传进来:

const result = renderFormDsl(canvas, dsl, { width: card.clientWidth });
// 或者只要组件树
const compiled = compileFormDsl(dsl, { width: card.clientWidth });

不传则退回 dsl.width,再退回 360。宿主给的宽度会被 maxWidth(默认 640) 夹住 —— 把 896 全铺满不是"排满了",是难看:一行 896 宽的输入框没人读得过来。 (maxWidth: Infinity 就是不设上限;也可以用 dsl.maxWidth 在文档里声明。)

表单最终多宽result.width —— 宿主需要它来决定画布/容器多宽。 拿自己传进去的宽度去定画布就会宽出一截、右边空一块,看起来跟没修一样。

容器尺寸变了要调 setWidth() —— resize() 管画布,setWidth() 管内容,两件事都要做:

window.addEventListener('resize', () => {
  result.setWidth(card.clientWidth);   // 表单与控件重新对齐(同样会被 maxWidth 夹住)
  result.resize(result.width, height); // 画布跟**表单实际宽度**,不是容器宽度
});

为什么宽度要一层层显式写下去:宽度在 ICE 里是每个组件自己的属性, 没有"父级拉满"的自动传导ICEFormICEBoxLayout({ align: 'stretch' }) 拉的是 ICEFormItem,不拉控件;而 ICEFormItem.doLayout 只按 control.state.width (缺省 200定位控件,不改变它。所以只写 align: 'stretch' 对视觉结果完全没有作用, 每个控件会落到自己的出厂默认 —— ICETextField 200、ICEInputNumber 140、ICESelect 200, 同一张表单里几个控件宽度还互不相同(实测宿主 896 宽时右侧空掉 667px,74%)。

编译期给的默认规则:

| 字段类型 | 控件宽度 | | --- | --- | | 除 number 以外全部 | 表单宽度(horizontal 布局下是 max(120, 表单宽度 - 80),那 80 是 ICEFormItem 的默认 labelWidth,让给标签) | | number | 200,不拉伸 |

number 是唯一的例外,理由是 ICEInputNumber 自己的内部布局:减号贴最左端、数值居中, 宽度一拉大这两样就天各一方(实测 890px 时看着像坏了)。

反过来,其余类型必须拉伸,因为它们的出厂默认是退化的slider 默认 10px、checkbox 默认 0px、radio-group 默认 35px —— 不给宽度就会画出一个看不见的控件。(这一条是实测出来的:第一版只让"文本类"拉伸, 结果滑块变成 10px 宽的一条。)

逐字段写的 width 优先级最高,且 setWidth() 不会动它 —— 显式意图不该被重排抹掉。

9. 诊断码

validate 返回的每条诊断都带 codepath,便于程序化处理与回灌给 agent。 共 38 个错误码 + 6 个警告码

错误(会导致不渲染):

default-not-in-options · default-type-mismatch · duplicate-name · duplicate-option-value · empty-fields · empty-options · field-constraint-not-number · field-not-object · invalid-dependencies · invalid-dependency · invalid-direction · invalid-fields · invalid-layout · invalid-mode · invalid-name · invalid-options · invalid-pattern · invalid-placement · invalid-root · invalid-rule · invalid-rules · length-range-inverted · missing-fields · missing-kind · missing-name · missing-type · option-invalid · option-missing-value · option-value-not-string · options-required · range-inverted · required-not-boolean · rule-value-not-number · self-dependency · unknown-dependency · unsupported-field-type · unsupported-kind · unsupported-schema-version

警告(会渲染,但值得看一眼):

invalid-number · invalid-submit-text · options-not-allowed · rule-not-applicable · unknown-field · unknown-rule-key

每条错误都尽量带上可操作的替代信息

[错误] 不支持的字段类型「richtext」。可用类型:text / textarea / password / number / slider / …(fields[0].type)
[错误] 依赖的字段「passwrod」不存在。可用字段:password / confirm。(fields[1].dependencies[0])
[错误] 默认值「auto」不在 options 里。可用取值:on / off。(fields[0].default)
[警告] 「number」不认识字段属性「nope」会被忽略。它接受:step / precision,以及通用属性 name / type / …

10. 逃生舱

DSL 只覆盖能写成 JSON 的部分。需要更强的东西时,在编译产物上继续做

const { model, form } = compileFormDsl(dsl);

// 自定义同步校验
model.getField('name')!.rules!.push({
  validator: (value) => (value === 'admin' ? '这个名字被占用了' : null),
});

// 异步校验(DSL 表达不了;按钮走 submitAsync,会先同步后异步)
model.getField('name')!.rules!.push({
  asyncValidator: (value) => fetch('/api/check?name=' + value).then((r) => r.json()).then((d) => (d.taken ? '已占用' : null)),
});

// 换个日期显示格式(原生的 format 是函数)
form.getItems()[3].getControl().setFormat((v) => v.replace(/-/g, '/'));

字段上的 props 也直通控件构造函数(内容不校验)。

11. Agent 发现路径

Agent 可以通过这几处使用本项目:

  1. npm 包导出(validateFormDsl / compileFormDsl / renderFormDsl
  2. AGENTS.md
  3. skills/ice-web-components-dsl/SKILL.md —— 完整规范(§7「库里还有什么」由清单自动生成)
  4. prompts/agent-prompt.md —— 短版系统提示词(输出契约 + 自检清单)
  5. src/schema/form-dsl.schema.json —— JSON Schema
  6. catalog/components.json —— 组件清单(机器可读,见 §11.1)

README、SKILL 与提示词里的 JSON 例子由 tests/doc-examples.test.ts 自动校验: 示例一旦不合法(类型写错、依赖悬空、选项为空),测试就红 —— 保证 Agent 照抄的是"能跑的文档"。

示例页:examples/form-dsl.html(带 JSON 编辑器与诊断面板,含四个预设:合法 / 类型写错 / 选项有问题 / 依赖指向空)。

11.1 组件清单:让 agent 知道"库里还有什么"

这份 DSL 只覆盖 11 个字段类型,而 ice-web-components 有 84 个 UI 组件。 agent 的真正瓶颈不是画布,是它不知道自己有什么可选 —— 之前 SKILL 里只有那 11 行表, 剩下的它看不见,于是"要个日期区间"也只能退回两个 date 字段。

catalog/components.jsonnpm run catalog 生成)把整库摊开:分组、摘要、构造参数、 方法、值的形状,以及本包加的标注 —— 哪些已接入、哪些能接但还没接、哪些根本不是字段。

三条边界,都不是随手定的:

  1. 分组与摘要来自上游的生成产物,不是我自己列的。 上游 scripts/gen-docs.mjs 已经有一套抽取器,而且有 docs:check 门禁保证 "每个组件都被登记过"。本包再写一个 TypeScript 解析器就是第二份抽取器, 两份会各自漂移,而漂移的症状是"清单里少了个字段"这种没人会发现的形态。 所以这里只做搬运与重组(用法见 tools/gen-catalog.mjs 顶部注释)。
  2. "能不能当字段"是手写的 —— 那是编辑判断,源码里推不出来。 但两侧都有门禁:标注里的组件名要真实存在、fieldType 要合法, 且每个已实现的类型都必须有组件认领(加了类型却忘了说它由谁实现 → 测试红)。
  3. 清单自己知道缺什么。 gaps 一节列出"构造参数没进生成文档的类"(48/113,含 ICEButton / ICETextField 这些最常用的 —— 它们继承基类的 Options,或构造函数就是 props?: any)和"上游没写类注释的条目"。空数组不等于"没有参数", 所以宁可把缺口列出来,也不糊一个 [] 过去。

SKILL 的 §7 由同一份清单生成(标记块 <!-- catalog:start -->)。 npm run verify 里带 catalog 重生成 + tests/catalog.test.ts 的同步门禁 —— 上游一改、或本包标注一改而忘了重生成,测试就红

12. 验证

npm run verify        # types:check + build + jest
npm run verify:full   # 上面 + playwright(示例页真机冒烟)

当前规模:132 单测 / 7 套件10 e2e / 1 spec20 个字段类型,清单覆盖 84 个 UI 组件(9 组 / 185 个条目)。

示例页 e2e 的判据不是"按钮存在",而是:画布上真的有墨、 真实点中画布上的提交按钮能走完校验 → 提交这条链、诊断里带可操作的替代信息。

其中三例按着墨包围盒判排布("表单铺满内容宽度" / "宽屏上停在 maxWidth" / "窗口变窄后跟着重新对齐")—— countInk 那类"画了没有"的断言抓不到 "画出来了但只占左边一小块":着墨量照样几千。这一组是实测缺陷的回归,见 §8.1。

判据要两边都判。"占画布 x%" 这类单边判据对"把整张卡片全铺满"照样成立, 所以那两条用例同时断言"不缩成一小块""不拉满整张卡片"。 这一点是 A/B 时发现的:故意把 maxWidth 传成 Infinity,用例居然还是绿的。

知道这个数字从哪来很重要:宽度这条链上没有一处会报错。 修复前宿主 896 宽、表单只画了 229px —— 校验通过、编译通过、渲染成功, 只有人会看出"右边怎么空了那么多"。

13. 与家族其它包的关系

| 包 | 面向 | 补的是什么 | | --- | --- | --- | | ice-chart-dsl | Agent 画图 | 一张表 + encoding;轴/图例/提示框的意图级默认 | | ice-render-dsl | Agent 画图形 | 节点/连线 DSL + 编排 | | ice-entity-designer-dsl | Agent 建数据模型 | 实体/关系 DSL | | ice-web-components-dsl | Agent 收集输入 | 扁平字段表;约束"写一次两处生效";表单诊断 |

四个 DSL 是同一套口径:JSON 输入、编译成原生产物、诊断可回灌。 它们与 MCP / AG-UI 的关系是"同一份声明,两条通道" —— MCP 让 agent 能生成, AG-UI 把生成结果送到用户屏幕上。

14. 许可

MIT