@yaoxiu/marketing-dsl 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@ import { createRuntime } from '@yaoxiu/marketing-dsl';
47
47
 
48
48
  const runtime = createRuntime(dsl, {
49
49
  user: { shopName: '麦爆了旗舰店', version: '专业版' },
50
- sources: { listRenewTiers: (params) => api.getTiers(params) },
50
+ sources: { listRenewTiers: params => api.getTiers(params) },
51
51
  handlers: { setNeverRemind: () => localStorage.setItem('never', '1') },
52
52
  emit: (event, payload) => console.log(event, payload),
53
53
  });
@@ -56,11 +56,18 @@ const unsubscribe = runtime.subscribe(() => {
56
56
  render(runtime.getTree()); // 每次通知后重新取树画一遍
57
57
  });
58
58
 
59
+ // 用户信息异步到了、或者要切换编辑态,用 update 就地更新。
60
+ // 不要为此重建 runtime——重建会重新拉数据源、重放曝光埋点,
61
+ // 还会把用户已经切到的 tab 打回默认值。
62
+ runtime.update({ user: { shopName: '麦爆了旗舰店' } });
63
+
59
64
  // 卸载时必须调,否则倒计时定时器不会停
60
65
  runtime.destroy();
61
66
  unsubscribe();
62
67
  ```
63
68
 
69
+ `update` 能改 `user` / `handlers` / `editMode`。换 `dsl` 本身要重建 runtime。
70
+
64
71
  ## 渲染树
65
72
 
66
73
  `getTree()` 返回的是纯数据,所有解释工作都已经做完:插值算完了、条件判断过了、循环展开了、样式转成 CSS 了、坐标换算好了、动作绑好了。
@@ -97,14 +104,14 @@ unsubscribe();
97
104
 
98
105
  这几条是设计红线,不会因为「就差一点点」而放开:
99
106
 
100
- | 边界 | 做法 |
101
- | --- | --- |
102
- | 不执行任意代码 | 手写解析器,不用 `eval` / `new Function`;不支持函数调用和赋值 |
103
- | 不能沿原型链逃逸 | `constructor` / `__proto__` / `prototype` 一律读不到,函数值也读不到 |
104
- | 不能通过样式作恶 | 只有白名单里的属性会输出,`position: fixed` 这类会被丢弃 |
105
- | 不能执行 `javascript:` | 链接只允许 `http` / `https` / `mailto` / `tel` 和相对路径 |
106
- | 不能打任意接口 | 数据源用注册名引用,配置里写不了 URL |
107
- | 不能调任意方法 | `call` 只能调宿主注册在 `handlers` 里的方法 |
107
+ | 边界 | 做法 |
108
+ | ---------------------- | -------------------------------------------------------------------- |
109
+ | 不执行任意代码 | 手写解析器,不用 `eval` / `new Function`;不支持函数调用和赋值 |
110
+ | 不能沿原型链逃逸 | `constructor` / `__proto__` / `prototype` 一律读不到,函数值也读不到 |
111
+ | 不能通过样式作恶 | 只有白名单里的属性会输出,`position: fixed` 这类会被丢弃 |
112
+ | 不能执行 `javascript:` | 链接只允许 `http` / `https` / `mailto` / `tel` 和相对路径 |
113
+ | 不能打任意接口 | 数据源用注册名引用,配置里写不了 URL |
114
+ | 不能调任意方法 | `call` 只能调宿主注册在 `handlers` 里的方法 |
108
115
 
109
116
  ## 完整 DSL 语法
110
117
 
package/dist/index.cjs CHANGED
@@ -326,9 +326,11 @@ function safeImageUrl(input) {
326
326
 
327
327
  // src/actions.ts
328
328
  function createDispatcher(options) {
329
- const { setState, emit, openView, closeTop, closeAll, handlers = {}, editMode = false } = options;
329
+ const { setState, emit, openView, closeTop, closeAll } = options;
330
330
  function dispatch(action, context) {
331
331
  if (!action || !action.type) return;
332
+ const handlers = options.handlers || {};
333
+ const editMode = !!options.editMode;
332
334
  switch (action.type) {
333
335
  case "sequence":
334
336
  (action.actions || []).forEach((item) => dispatch(item, context));
@@ -608,7 +610,13 @@ function resolveTree(input) {
608
610
  );
609
611
  const countdowns = { endTimes: [], precision: "s" };
610
612
  const renderLayers = ready ? layers.map(
611
- (item, index) => resolveLayer(item.name, item.view, index === layers.length - 1, input, countdowns)
613
+ (item, index) => resolveLayer(
614
+ item.name,
615
+ item.view,
616
+ index === layers.length - 1,
617
+ input,
618
+ countdowns
619
+ )
612
620
  ) : [];
613
621
  return {
614
622
  ready: ready && layers.length > 0,
@@ -627,7 +635,9 @@ function resolveLayer(name, view, isTop, input, countdowns) {
627
635
  const rootLayout = stage.layout === "flow" ? "flow" : "absolute";
628
636
  const maskClosable = !!stage.maskClosable;
629
637
  const onMaskClick = maskClosable ? () => closeTop("mask") : void 0;
630
- const nodes = (view.nodes || []).map((node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)).filter((el) => !!el);
638
+ const nodes = (view.nodes || []).map(
639
+ (node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)
640
+ ).filter((el) => !!el);
631
641
  return {
632
642
  name,
633
643
  type: view.type,
@@ -690,8 +700,12 @@ function resolveStageStyle(stage, isPopup, context) {
690
700
  base,
691
701
  {
692
702
  // width / height 支持数字(px)、'100%' / 'auto' / calc(...) 等相对单位,也支持 {{ }}
693
- width: toLength(interpolate(stage.width === void 0 ? 320 : stage.width, context)),
694
- height: toLength(interpolate(stage.height === void 0 ? 400 : stage.height, context))
703
+ width: toLength(
704
+ interpolate(stage.width === void 0 ? 320 : stage.width, context)
705
+ ),
706
+ height: toLength(
707
+ interpolate(stage.height === void 0 ? 400 : stage.height, context)
708
+ )
695
709
  },
696
710
  // stage.style 也走一遍插值,这样切 tab 时弹窗本身的背景能跟着变
697
711
  toCssStyle(interpolateDeep(stage.style, context))
@@ -737,14 +751,21 @@ function resolveCloseButton(config, layerName, closeTop) {
737
751
  return {
738
752
  key: `${layerName}-close`,
739
753
  tag: "div",
754
+ className: "dsl-close",
740
755
  style,
741
756
  onClick,
742
757
  children: [
743
758
  {
744
759
  key: `${layerName}-close-img`,
745
760
  tag: "img",
761
+ className: "dsl-close-img",
746
762
  src: image,
747
- style: { width: "100%", height: "100%", objectFit: "contain", display: "block" }
763
+ style: {
764
+ width: "100%",
765
+ height: "100%",
766
+ objectFit: "contain",
767
+ display: "block"
768
+ }
748
769
  }
749
770
  ]
750
771
  };
@@ -752,12 +773,27 @@ function resolveCloseButton(config, layerName, closeTop) {
752
773
  return {
753
774
  key: `${layerName}-close`,
754
775
  tag: "div",
776
+ className: "dsl-close",
755
777
  style,
756
778
  onClick,
757
779
  text: config.icon || "\xD7"
758
780
  };
759
781
  }
760
782
  var BASE_STYLE = { boxSizing: "border-box" };
783
+ var NODE_CLASS = {
784
+ box: "dsl-box",
785
+ flex: "dsl-flex",
786
+ repeat: "dsl-flex",
787
+ tabs: "dsl-tabs",
788
+ image: "dsl-image",
789
+ text: "dsl-text",
790
+ button: "dsl-button",
791
+ countdown: "dsl-flex"
792
+ };
793
+ function nodeClass(type, clickable) {
794
+ const base = `dsl-node ${NODE_CLASS[type] || ""}`.trim();
795
+ return clickable ? `${base} is-clickable` : base;
796
+ }
761
797
  function resolveNode(node, key, layout, context, input, countdowns) {
762
798
  if (!node || typeof node !== "object") return void 0;
763
799
  if (node.visibleWhen && !evaluate(node.visibleWhen, context)) return void 0;
@@ -776,14 +812,23 @@ function resolveNode(node, key, layout, context, input, countdowns) {
776
812
  return {
777
813
  key,
778
814
  tag: "div",
815
+ className: nodeClass("box", !!onClick),
779
816
  style: Object.assign({ position: "relative" }, style),
780
817
  onClick,
781
- children: resolveChildren(node.children, key, "absolute", context, input, countdowns)
818
+ children: resolveChildren(
819
+ node.children,
820
+ key,
821
+ "absolute",
822
+ context,
823
+ input,
824
+ countdowns
825
+ )
782
826
  };
783
827
  case "flex":
784
828
  return {
785
829
  key,
786
830
  tag: "div",
831
+ className: nodeClass("flex", !!onClick),
787
832
  style: Object.assign({ display: "flex" }, style),
788
833
  onClick,
789
834
  children: resolveChildren(node.children, key, "flow", context, input, countdowns)
@@ -792,6 +837,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
792
837
  return {
793
838
  key,
794
839
  tag: "div",
840
+ className: nodeClass("repeat", !!onClick),
795
841
  style: Object.assign({ display: "flex" }, style),
796
842
  onClick,
797
843
  children: resolveRepeat(node, key, context, input, countdowns)
@@ -800,6 +846,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
800
846
  return {
801
847
  key,
802
848
  tag: "div",
849
+ className: nodeClass("tabs", false),
803
850
  style: Object.assign({ display: "flex" }, style),
804
851
  children: resolveTabs(node, key, context, input)
805
852
  };
@@ -807,6 +854,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
807
854
  return {
808
855
  key,
809
856
  tag: "img",
857
+ className: nodeClass("image", !!onClick),
810
858
  src: safeImageUrl(interpolate(node.src, context)),
811
859
  style: Object.assign({ display: "block", objectFit: "cover" }, style),
812
860
  onClick
@@ -815,6 +863,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
815
863
  return {
816
864
  key,
817
865
  tag: "div",
866
+ className: nodeClass("text", !!onClick),
818
867
  style: Object.assign({ wordBreak: "break-word" }, style),
819
868
  text: toText(interpolate(node.content, context)),
820
869
  onClick
@@ -823,6 +872,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
823
872
  return {
824
873
  key,
825
874
  tag: "div",
875
+ className: nodeClass("button", true),
826
876
  style: Object.assign(
827
877
  {
828
878
  display: "flex",
@@ -857,7 +907,14 @@ function resolveRepeat(node, key, context, input, countdowns) {
857
907
  [node.itemName || "item"]: item,
858
908
  [node.indexName || "index"]: index
859
909
  });
860
- return resolveNode(node.template, `${key}-${index}`, "flow", itemContext, input, countdowns);
910
+ return resolveNode(
911
+ node.template,
912
+ `${key}-${index}`,
913
+ "flow",
914
+ itemContext,
915
+ input,
916
+ countdowns
917
+ );
861
918
  }).filter((el) => !!el);
862
919
  }
863
920
  function resolveTabs(node, key, context, input) {
@@ -873,6 +930,7 @@ function resolveTabs(node, key, context, input) {
873
930
  return {
874
931
  key: `${key}-${index}`,
875
932
  tag: "div",
933
+ className: index === active ? "dsl-tab-item is-active" : "dsl-tab-item",
876
934
  style: Object.assign(
877
935
  {
878
936
  display: "flex",
@@ -909,6 +967,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
909
967
  return {
910
968
  key,
911
969
  tag: "div",
970
+ className: "dsl-node dsl-text dsl-countdown",
912
971
  style: Object.assign({ wordBreak: "break-word" }, style),
913
972
  text: node.endText || "\u5DF2\u7ED3\u675F"
914
973
  };
@@ -917,6 +976,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
917
976
  return {
918
977
  key,
919
978
  tag: "div",
979
+ className: "dsl-node dsl-text dsl-countdown",
920
980
  style: Object.assign({ wordBreak: "break-word" }, style),
921
981
  text: formatParts(parts, node.format)
922
982
  };
@@ -925,8 +985,16 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
925
985
  return {
926
986
  key,
927
987
  tag: "div",
988
+ className: "dsl-node dsl-flex dsl-countdown",
928
989
  style: Object.assign({ display: "flex" }, style),
929
- children: resolveChildren(node.children, key, "flow", childContext, input, countdowns)
990
+ children: resolveChildren(
991
+ node.children,
992
+ key,
993
+ "flow",
994
+ childContext,
995
+ input,
996
+ countdowns
997
+ )
930
998
  };
931
999
  }
932
1000
  function toText(value) {
@@ -937,7 +1005,12 @@ function toText(value) {
937
1005
  var noopEmit = () => {
938
1006
  };
939
1007
  function createRuntime(dsl, options = {}) {
940
- const { user = {}, sources = {}, handlers = {}, editMode = false, emit = noopEmit } = options;
1008
+ const { sources = {}, emit = noopEmit } = options;
1009
+ const live = {
1010
+ user: options.user || {},
1011
+ handlers: options.handlers || {},
1012
+ editMode: !!options.editMode
1013
+ };
941
1014
  const normalized = normalizeViews(dsl);
942
1015
  let state = Object.assign({}, dsl.state || {});
943
1016
  let resolvedData = {};
@@ -953,7 +1026,10 @@ function createRuntime(dsl, options = {}) {
953
1026
  };
954
1027
  const ticker = createTicker({ onTick: notify });
955
1028
  function buildContext() {
956
- const context = Object.assign({}, resolvedData, { state, user });
1029
+ const context = Object.assign({}, resolvedData, {
1030
+ state,
1031
+ user: live.user
1032
+ });
957
1033
  const derived = dsl.derived || {};
958
1034
  Object.keys(derived).forEach((name) => {
959
1035
  const config = derived[name] || {};
@@ -1014,8 +1090,12 @@ function createRuntime(dsl, options = {}) {
1014
1090
  // 配置里 close 可以自带 reason(如 'never-remind'),要透传给埋点,不能吞掉
1015
1091
  closeTop: (reason) => closeTop(reason),
1016
1092
  closeAll: (reason) => closeAll(reason),
1017
- handlers,
1018
- editMode
1093
+ get handlers() {
1094
+ return live.handlers;
1095
+ },
1096
+ get editMode() {
1097
+ return live.editMode;
1098
+ }
1019
1099
  });
1020
1100
  function fireLifecycle(viewName, hook, extra) {
1021
1101
  const view = normalized.views[viewName];
@@ -1042,7 +1122,7 @@ function createRuntime(dsl, options = {}) {
1042
1122
  return;
1043
1123
  }
1044
1124
  pending.push(
1045
- Promise.resolve(source(ref.params || {}, user)).then((result) => {
1125
+ Promise.resolve(source(ref.params || {}, live.user)).then((result) => {
1046
1126
  resolved[key] = result;
1047
1127
  })
1048
1128
  );
@@ -1080,6 +1160,12 @@ function createRuntime(dsl, options = {}) {
1080
1160
  }
1081
1161
  return {
1082
1162
  getTree,
1163
+ update(patch) {
1164
+ if (patch.user !== void 0) live.user = patch.user;
1165
+ if (patch.handlers !== void 0) live.handlers = patch.handlers;
1166
+ if (patch.editMode !== void 0) live.editMode = patch.editMode;
1167
+ notify();
1168
+ },
1083
1169
  subscribe(listener) {
1084
1170
  listeners.add(listener);
1085
1171
  return () => listeners.delete(listener);
@@ -1158,7 +1244,10 @@ function validate(dsl) {
1158
1244
  }
1159
1245
  const doc = dsl;
1160
1246
  if (doc.version !== DSL_VERSION) {
1161
- add("version", `version \u5FC5\u987B\u662F ${DSL_VERSION}\uFF0C\u5F53\u524D\u4E3A ${JSON.stringify(doc.version)}`);
1247
+ add(
1248
+ "version",
1249
+ `version \u5FC5\u987B\u662F ${DSL_VERSION}\uFF0C\u5F53\u524D\u4E3A ${JSON.stringify(doc.version)}`
1250
+ );
1162
1251
  }
1163
1252
  const dataKeys = validateData(doc.data, add);
1164
1253
  const stateKeys = validateState(doc.state, add);
@@ -1258,7 +1347,10 @@ function validateStage(stage, add, warn, ctx, prefix) {
1258
1347
  if (stage.onShow !== void 0) validateAction(stage.onShow, at("onShow"), ctx);
1259
1348
  if (stage.onClose !== void 0) validateAction(stage.onClose, at("onClose"), ctx);
1260
1349
  if (stage.height === "auto" && stage.layout !== "flow") {
1261
- add("stage.height", "height: 'auto' \u9700\u8981\u540C\u65F6\u8BBE\u7F6E stage.layout: 'flow'\uFF0C\u5426\u5219\u9AD8\u5EA6\u4F1A\u584C\u6210 0");
1350
+ add(
1351
+ "stage.height",
1352
+ "height: 'auto' \u9700\u8981\u540C\u65F6\u8BBE\u7F6E stage.layout: 'flow'\uFF0C\u5426\u5219\u9AD8\u5EA6\u4F1A\u584C\u6210 0"
1353
+ );
1262
1354
  }
1263
1355
  }
1264
1356
  function validateCloseButton(config, add, warn) {
@@ -1272,7 +1364,10 @@ function validateCloseButton(config, add, warn) {
1272
1364
  }
1273
1365
  if (config.offset !== void 0) {
1274
1366
  if (!Array.isArray(config.offset) || config.offset.length !== 2) {
1275
- add("stage.closeButton.offset", "offset \u5FC5\u987B\u662F [x, y] \u4E24\u9879\u6570\u7EC4\uFF0C\u8D1F\u503C\u4F1A\u628A\u6309\u94AE\u79FB\u5230\u5F39\u7A97\u5916\u9762");
1367
+ add(
1368
+ "stage.closeButton.offset",
1369
+ "offset \u5FC5\u987B\u662F [x, y] \u4E24\u9879\u6570\u7EC4\uFF0C\u8D1F\u503C\u4F1A\u628A\u6309\u94AE\u79FB\u5230\u5F39\u7A97\u5916\u9762"
1370
+ );
1276
1371
  } else {
1277
1372
  config.offset.forEach((value, index) => {
1278
1373
  if (!checkLength(value)) {
@@ -1293,7 +1388,10 @@ function validateCloseButton(config, add, warn) {
1293
1388
  if (config.style) {
1294
1389
  Object.keys(config.style).forEach((key) => {
1295
1390
  if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1296
- warn(`stage.closeButton.style.${key}`, `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`);
1391
+ warn(
1392
+ `stage.closeButton.style.${key}`,
1393
+ `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
1394
+ );
1297
1395
  }
1298
1396
  });
1299
1397
  }
@@ -1364,7 +1462,10 @@ function validateNode(node, path, layout, ctx) {
1364
1462
  } else {
1365
1463
  node.rect.forEach((value, index) => {
1366
1464
  if (value !== void 0 && value !== null && !checkLength(value)) {
1367
- add(`${path}.rect[${index}]`, `"${value}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`);
1465
+ add(
1466
+ `${path}.rect[${index}]`,
1467
+ `"${value}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`
1468
+ );
1368
1469
  }
1369
1470
  });
1370
1471
  }
@@ -1382,7 +1483,10 @@ function validateNode(node, path, layout, ctx) {
1382
1483
  );
1383
1484
  }
1384
1485
  if (key === "aspectRatio" && !checkAspectRatio(node.style[key])) {
1385
- add(`${path}.style.aspectRatio`, "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4");
1486
+ add(
1487
+ `${path}.style.aspectRatio`,
1488
+ "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
1489
+ );
1386
1490
  }
1387
1491
  });
1388
1492
  }
@@ -1412,7 +1516,8 @@ function validateNodeByType(node, path, ctx) {
1412
1516
  if (typeof node.bind !== "string") {
1413
1517
  add(`${path}.bind`, "tabs \u5FC5\u987B\u63D0\u4F9B bind\uFF08data \u4E2D\u7684\u6570\u7EC4\u5B57\u6BB5\u540D\uFF09");
1414
1518
  }
1415
- if (typeof node.stateKey !== "string") add(`${path}.stateKey`, "tabs \u5FC5\u987B\u63D0\u4F9B stateKey");
1519
+ if (typeof node.stateKey !== "string")
1520
+ add(`${path}.stateKey`, "tabs \u5FC5\u987B\u63D0\u4F9B stateKey");
1416
1521
  if (node.labelField !== void 0 && typeof node.labelField !== "string") {
1417
1522
  add(`${path}.labelField`, "labelField \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
1418
1523
  }
@@ -1478,7 +1583,10 @@ function validateAction(action, path, ctx) {
1478
1583
  add(`${path}.view`, `views \u91CC\u4E0D\u5B58\u5728\u89C6\u56FE "${action.view}"`);
1479
1584
  }
1480
1585
  if (action.mode !== void 0 && ["stack", "replace"].indexOf(action.mode) === -1) {
1481
- add(`${path}.mode`, "mode \u53EA\u80FD\u662F 'stack'\uFF08\u9ED8\u8BA4\uFF0C\u53E0\u4E00\u5C42\uFF09\u6216 'replace'\uFF08\u6362\u6389\u5F53\u524D\u5C42\uFF09");
1586
+ add(
1587
+ `${path}.mode`,
1588
+ "mode \u53EA\u80FD\u662F 'stack'\uFF08\u9ED8\u8BA4\uFF0C\u53E0\u4E00\u5C42\uFF09\u6216 'replace'\uFF08\u6362\u6389\u5F53\u524D\u5C42\uFF09"
1589
+ );
1482
1590
  }
1483
1591
  }
1484
1592
  if (action.type === "sequence") {
package/dist/index.d.cts CHANGED
@@ -153,6 +153,12 @@ interface RenderElement {
153
153
  key: string;
154
154
  /** 画成什么。div 带 text 就是文本节点,带 children 就是容器 */
155
155
  tag: 'div' | 'img';
156
+ /**
157
+ * 类名,如 "dsl-node dsl-text is-clickable"。
158
+ * 不参与样式(样式全在 style 里),纯粹是留给调试和宿主微调的钩子。
159
+ * 由 core 统一生成,不让各框架壳各拼一套,否则 DOM 结构会慢慢跑偏。
160
+ */
161
+ className: string;
156
162
  style: CssStyle;
157
163
  text?: string;
158
164
  src?: string;
@@ -287,6 +293,16 @@ interface RuntimeOptions {
287
293
  interface DslRuntime {
288
294
  /** 当前渲染树。每次 subscribe 回调后重新取 */
289
295
  getTree: () => RenderTree;
296
+ /**
297
+ * 更新宿主传进来的东西。
298
+ *
299
+ * user 常常是异步到的(弹窗先挂载、用户信息后到),
300
+ * 不给更新入口的话文案就永远停在初始值。
301
+ * editMode / handlers 同理,宿主可能中途换。
302
+ *
303
+ * 不会重新拉数据源,也不会重放 onShow 曝光埋点——那些是一次性的。
304
+ */
305
+ update: (patch: Pick<RuntimeOptions, 'user' | 'handlers' | 'editMode'>) => void;
290
306
  /** 状态变化时回调,返回取消订阅的函数 */
291
307
  subscribe: (listener: () => void) => () => void;
292
308
  /** 供调试台等外部读取,正常渲染不需要 */
package/dist/index.d.ts CHANGED
@@ -153,6 +153,12 @@ interface RenderElement {
153
153
  key: string;
154
154
  /** 画成什么。div 带 text 就是文本节点,带 children 就是容器 */
155
155
  tag: 'div' | 'img';
156
+ /**
157
+ * 类名,如 "dsl-node dsl-text is-clickable"。
158
+ * 不参与样式(样式全在 style 里),纯粹是留给调试和宿主微调的钩子。
159
+ * 由 core 统一生成,不让各框架壳各拼一套,否则 DOM 结构会慢慢跑偏。
160
+ */
161
+ className: string;
156
162
  style: CssStyle;
157
163
  text?: string;
158
164
  src?: string;
@@ -287,6 +293,16 @@ interface RuntimeOptions {
287
293
  interface DslRuntime {
288
294
  /** 当前渲染树。每次 subscribe 回调后重新取 */
289
295
  getTree: () => RenderTree;
296
+ /**
297
+ * 更新宿主传进来的东西。
298
+ *
299
+ * user 常常是异步到的(弹窗先挂载、用户信息后到),
300
+ * 不给更新入口的话文案就永远停在初始值。
301
+ * editMode / handlers 同理,宿主可能中途换。
302
+ *
303
+ * 不会重新拉数据源,也不会重放 onShow 曝光埋点——那些是一次性的。
304
+ */
305
+ update: (patch: Pick<RuntimeOptions, 'user' | 'handlers' | 'editMode'>) => void;
290
306
  /** 状态变化时回调,返回取消订阅的函数 */
291
307
  subscribe: (listener: () => void) => () => void;
292
308
  /** 供调试台等外部读取,正常渲染不需要 */
package/dist/index.js CHANGED
@@ -324,9 +324,11 @@ function safeImageUrl(input) {
324
324
 
325
325
  // src/actions.ts
326
326
  function createDispatcher(options) {
327
- const { setState, emit, openView, closeTop, closeAll, handlers = {}, editMode = false } = options;
327
+ const { setState, emit, openView, closeTop, closeAll } = options;
328
328
  function dispatch(action, context) {
329
329
  if (!action || !action.type) return;
330
+ const handlers = options.handlers || {};
331
+ const editMode = !!options.editMode;
330
332
  switch (action.type) {
331
333
  case "sequence":
332
334
  (action.actions || []).forEach((item) => dispatch(item, context));
@@ -606,7 +608,13 @@ function resolveTree(input) {
606
608
  );
607
609
  const countdowns = { endTimes: [], precision: "s" };
608
610
  const renderLayers = ready ? layers.map(
609
- (item, index) => resolveLayer(item.name, item.view, index === layers.length - 1, input, countdowns)
611
+ (item, index) => resolveLayer(
612
+ item.name,
613
+ item.view,
614
+ index === layers.length - 1,
615
+ input,
616
+ countdowns
617
+ )
610
618
  ) : [];
611
619
  return {
612
620
  ready: ready && layers.length > 0,
@@ -625,7 +633,9 @@ function resolveLayer(name, view, isTop, input, countdowns) {
625
633
  const rootLayout = stage.layout === "flow" ? "flow" : "absolute";
626
634
  const maskClosable = !!stage.maskClosable;
627
635
  const onMaskClick = maskClosable ? () => closeTop("mask") : void 0;
628
- const nodes = (view.nodes || []).map((node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)).filter((el) => !!el);
636
+ const nodes = (view.nodes || []).map(
637
+ (node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)
638
+ ).filter((el) => !!el);
629
639
  return {
630
640
  name,
631
641
  type: view.type,
@@ -688,8 +698,12 @@ function resolveStageStyle(stage, isPopup, context) {
688
698
  base,
689
699
  {
690
700
  // width / height 支持数字(px)、'100%' / 'auto' / calc(...) 等相对单位,也支持 {{ }}
691
- width: toLength(interpolate(stage.width === void 0 ? 320 : stage.width, context)),
692
- height: toLength(interpolate(stage.height === void 0 ? 400 : stage.height, context))
701
+ width: toLength(
702
+ interpolate(stage.width === void 0 ? 320 : stage.width, context)
703
+ ),
704
+ height: toLength(
705
+ interpolate(stage.height === void 0 ? 400 : stage.height, context)
706
+ )
693
707
  },
694
708
  // stage.style 也走一遍插值,这样切 tab 时弹窗本身的背景能跟着变
695
709
  toCssStyle(interpolateDeep(stage.style, context))
@@ -735,14 +749,21 @@ function resolveCloseButton(config, layerName, closeTop) {
735
749
  return {
736
750
  key: `${layerName}-close`,
737
751
  tag: "div",
752
+ className: "dsl-close",
738
753
  style,
739
754
  onClick,
740
755
  children: [
741
756
  {
742
757
  key: `${layerName}-close-img`,
743
758
  tag: "img",
759
+ className: "dsl-close-img",
744
760
  src: image,
745
- style: { width: "100%", height: "100%", objectFit: "contain", display: "block" }
761
+ style: {
762
+ width: "100%",
763
+ height: "100%",
764
+ objectFit: "contain",
765
+ display: "block"
766
+ }
746
767
  }
747
768
  ]
748
769
  };
@@ -750,12 +771,27 @@ function resolveCloseButton(config, layerName, closeTop) {
750
771
  return {
751
772
  key: `${layerName}-close`,
752
773
  tag: "div",
774
+ className: "dsl-close",
753
775
  style,
754
776
  onClick,
755
777
  text: config.icon || "\xD7"
756
778
  };
757
779
  }
758
780
  var BASE_STYLE = { boxSizing: "border-box" };
781
+ var NODE_CLASS = {
782
+ box: "dsl-box",
783
+ flex: "dsl-flex",
784
+ repeat: "dsl-flex",
785
+ tabs: "dsl-tabs",
786
+ image: "dsl-image",
787
+ text: "dsl-text",
788
+ button: "dsl-button",
789
+ countdown: "dsl-flex"
790
+ };
791
+ function nodeClass(type, clickable) {
792
+ const base = `dsl-node ${NODE_CLASS[type] || ""}`.trim();
793
+ return clickable ? `${base} is-clickable` : base;
794
+ }
759
795
  function resolveNode(node, key, layout, context, input, countdowns) {
760
796
  if (!node || typeof node !== "object") return void 0;
761
797
  if (node.visibleWhen && !evaluate(node.visibleWhen, context)) return void 0;
@@ -774,14 +810,23 @@ function resolveNode(node, key, layout, context, input, countdowns) {
774
810
  return {
775
811
  key,
776
812
  tag: "div",
813
+ className: nodeClass("box", !!onClick),
777
814
  style: Object.assign({ position: "relative" }, style),
778
815
  onClick,
779
- children: resolveChildren(node.children, key, "absolute", context, input, countdowns)
816
+ children: resolveChildren(
817
+ node.children,
818
+ key,
819
+ "absolute",
820
+ context,
821
+ input,
822
+ countdowns
823
+ )
780
824
  };
781
825
  case "flex":
782
826
  return {
783
827
  key,
784
828
  tag: "div",
829
+ className: nodeClass("flex", !!onClick),
785
830
  style: Object.assign({ display: "flex" }, style),
786
831
  onClick,
787
832
  children: resolveChildren(node.children, key, "flow", context, input, countdowns)
@@ -790,6 +835,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
790
835
  return {
791
836
  key,
792
837
  tag: "div",
838
+ className: nodeClass("repeat", !!onClick),
793
839
  style: Object.assign({ display: "flex" }, style),
794
840
  onClick,
795
841
  children: resolveRepeat(node, key, context, input, countdowns)
@@ -798,6 +844,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
798
844
  return {
799
845
  key,
800
846
  tag: "div",
847
+ className: nodeClass("tabs", false),
801
848
  style: Object.assign({ display: "flex" }, style),
802
849
  children: resolveTabs(node, key, context, input)
803
850
  };
@@ -805,6 +852,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
805
852
  return {
806
853
  key,
807
854
  tag: "img",
855
+ className: nodeClass("image", !!onClick),
808
856
  src: safeImageUrl(interpolate(node.src, context)),
809
857
  style: Object.assign({ display: "block", objectFit: "cover" }, style),
810
858
  onClick
@@ -813,6 +861,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
813
861
  return {
814
862
  key,
815
863
  tag: "div",
864
+ className: nodeClass("text", !!onClick),
816
865
  style: Object.assign({ wordBreak: "break-word" }, style),
817
866
  text: toText(interpolate(node.content, context)),
818
867
  onClick
@@ -821,6 +870,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
821
870
  return {
822
871
  key,
823
872
  tag: "div",
873
+ className: nodeClass("button", true),
824
874
  style: Object.assign(
825
875
  {
826
876
  display: "flex",
@@ -855,7 +905,14 @@ function resolveRepeat(node, key, context, input, countdowns) {
855
905
  [node.itemName || "item"]: item,
856
906
  [node.indexName || "index"]: index
857
907
  });
858
- return resolveNode(node.template, `${key}-${index}`, "flow", itemContext, input, countdowns);
908
+ return resolveNode(
909
+ node.template,
910
+ `${key}-${index}`,
911
+ "flow",
912
+ itemContext,
913
+ input,
914
+ countdowns
915
+ );
859
916
  }).filter((el) => !!el);
860
917
  }
861
918
  function resolveTabs(node, key, context, input) {
@@ -871,6 +928,7 @@ function resolveTabs(node, key, context, input) {
871
928
  return {
872
929
  key: `${key}-${index}`,
873
930
  tag: "div",
931
+ className: index === active ? "dsl-tab-item is-active" : "dsl-tab-item",
874
932
  style: Object.assign(
875
933
  {
876
934
  display: "flex",
@@ -907,6 +965,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
907
965
  return {
908
966
  key,
909
967
  tag: "div",
968
+ className: "dsl-node dsl-text dsl-countdown",
910
969
  style: Object.assign({ wordBreak: "break-word" }, style),
911
970
  text: node.endText || "\u5DF2\u7ED3\u675F"
912
971
  };
@@ -915,6 +974,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
915
974
  return {
916
975
  key,
917
976
  tag: "div",
977
+ className: "dsl-node dsl-text dsl-countdown",
918
978
  style: Object.assign({ wordBreak: "break-word" }, style),
919
979
  text: formatParts(parts, node.format)
920
980
  };
@@ -923,8 +983,16 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
923
983
  return {
924
984
  key,
925
985
  tag: "div",
986
+ className: "dsl-node dsl-flex dsl-countdown",
926
987
  style: Object.assign({ display: "flex" }, style),
927
- children: resolveChildren(node.children, key, "flow", childContext, input, countdowns)
988
+ children: resolveChildren(
989
+ node.children,
990
+ key,
991
+ "flow",
992
+ childContext,
993
+ input,
994
+ countdowns
995
+ )
928
996
  };
929
997
  }
930
998
  function toText(value) {
@@ -935,7 +1003,12 @@ function toText(value) {
935
1003
  var noopEmit = () => {
936
1004
  };
937
1005
  function createRuntime(dsl, options = {}) {
938
- const { user = {}, sources = {}, handlers = {}, editMode = false, emit = noopEmit } = options;
1006
+ const { sources = {}, emit = noopEmit } = options;
1007
+ const live = {
1008
+ user: options.user || {},
1009
+ handlers: options.handlers || {},
1010
+ editMode: !!options.editMode
1011
+ };
939
1012
  const normalized = normalizeViews(dsl);
940
1013
  let state = Object.assign({}, dsl.state || {});
941
1014
  let resolvedData = {};
@@ -951,7 +1024,10 @@ function createRuntime(dsl, options = {}) {
951
1024
  };
952
1025
  const ticker = createTicker({ onTick: notify });
953
1026
  function buildContext() {
954
- const context = Object.assign({}, resolvedData, { state, user });
1027
+ const context = Object.assign({}, resolvedData, {
1028
+ state,
1029
+ user: live.user
1030
+ });
955
1031
  const derived = dsl.derived || {};
956
1032
  Object.keys(derived).forEach((name) => {
957
1033
  const config = derived[name] || {};
@@ -1012,8 +1088,12 @@ function createRuntime(dsl, options = {}) {
1012
1088
  // 配置里 close 可以自带 reason(如 'never-remind'),要透传给埋点,不能吞掉
1013
1089
  closeTop: (reason) => closeTop(reason),
1014
1090
  closeAll: (reason) => closeAll(reason),
1015
- handlers,
1016
- editMode
1091
+ get handlers() {
1092
+ return live.handlers;
1093
+ },
1094
+ get editMode() {
1095
+ return live.editMode;
1096
+ }
1017
1097
  });
1018
1098
  function fireLifecycle(viewName, hook, extra) {
1019
1099
  const view = normalized.views[viewName];
@@ -1040,7 +1120,7 @@ function createRuntime(dsl, options = {}) {
1040
1120
  return;
1041
1121
  }
1042
1122
  pending.push(
1043
- Promise.resolve(source(ref.params || {}, user)).then((result) => {
1123
+ Promise.resolve(source(ref.params || {}, live.user)).then((result) => {
1044
1124
  resolved[key] = result;
1045
1125
  })
1046
1126
  );
@@ -1078,6 +1158,12 @@ function createRuntime(dsl, options = {}) {
1078
1158
  }
1079
1159
  return {
1080
1160
  getTree,
1161
+ update(patch) {
1162
+ if (patch.user !== void 0) live.user = patch.user;
1163
+ if (patch.handlers !== void 0) live.handlers = patch.handlers;
1164
+ if (patch.editMode !== void 0) live.editMode = patch.editMode;
1165
+ notify();
1166
+ },
1081
1167
  subscribe(listener) {
1082
1168
  listeners.add(listener);
1083
1169
  return () => listeners.delete(listener);
@@ -1156,7 +1242,10 @@ function validate(dsl) {
1156
1242
  }
1157
1243
  const doc = dsl;
1158
1244
  if (doc.version !== DSL_VERSION) {
1159
- add("version", `version \u5FC5\u987B\u662F ${DSL_VERSION}\uFF0C\u5F53\u524D\u4E3A ${JSON.stringify(doc.version)}`);
1245
+ add(
1246
+ "version",
1247
+ `version \u5FC5\u987B\u662F ${DSL_VERSION}\uFF0C\u5F53\u524D\u4E3A ${JSON.stringify(doc.version)}`
1248
+ );
1160
1249
  }
1161
1250
  const dataKeys = validateData(doc.data, add);
1162
1251
  const stateKeys = validateState(doc.state, add);
@@ -1256,7 +1345,10 @@ function validateStage(stage, add, warn, ctx, prefix) {
1256
1345
  if (stage.onShow !== void 0) validateAction(stage.onShow, at("onShow"), ctx);
1257
1346
  if (stage.onClose !== void 0) validateAction(stage.onClose, at("onClose"), ctx);
1258
1347
  if (stage.height === "auto" && stage.layout !== "flow") {
1259
- add("stage.height", "height: 'auto' \u9700\u8981\u540C\u65F6\u8BBE\u7F6E stage.layout: 'flow'\uFF0C\u5426\u5219\u9AD8\u5EA6\u4F1A\u584C\u6210 0");
1348
+ add(
1349
+ "stage.height",
1350
+ "height: 'auto' \u9700\u8981\u540C\u65F6\u8BBE\u7F6E stage.layout: 'flow'\uFF0C\u5426\u5219\u9AD8\u5EA6\u4F1A\u584C\u6210 0"
1351
+ );
1260
1352
  }
1261
1353
  }
1262
1354
  function validateCloseButton(config, add, warn) {
@@ -1270,7 +1362,10 @@ function validateCloseButton(config, add, warn) {
1270
1362
  }
1271
1363
  if (config.offset !== void 0) {
1272
1364
  if (!Array.isArray(config.offset) || config.offset.length !== 2) {
1273
- add("stage.closeButton.offset", "offset \u5FC5\u987B\u662F [x, y] \u4E24\u9879\u6570\u7EC4\uFF0C\u8D1F\u503C\u4F1A\u628A\u6309\u94AE\u79FB\u5230\u5F39\u7A97\u5916\u9762");
1365
+ add(
1366
+ "stage.closeButton.offset",
1367
+ "offset \u5FC5\u987B\u662F [x, y] \u4E24\u9879\u6570\u7EC4\uFF0C\u8D1F\u503C\u4F1A\u628A\u6309\u94AE\u79FB\u5230\u5F39\u7A97\u5916\u9762"
1368
+ );
1274
1369
  } else {
1275
1370
  config.offset.forEach((value, index) => {
1276
1371
  if (!checkLength(value)) {
@@ -1291,7 +1386,10 @@ function validateCloseButton(config, add, warn) {
1291
1386
  if (config.style) {
1292
1387
  Object.keys(config.style).forEach((key) => {
1293
1388
  if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1294
- warn(`stage.closeButton.style.${key}`, `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`);
1389
+ warn(
1390
+ `stage.closeButton.style.${key}`,
1391
+ `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
1392
+ );
1295
1393
  }
1296
1394
  });
1297
1395
  }
@@ -1362,7 +1460,10 @@ function validateNode(node, path, layout, ctx) {
1362
1460
  } else {
1363
1461
  node.rect.forEach((value, index) => {
1364
1462
  if (value !== void 0 && value !== null && !checkLength(value)) {
1365
- add(`${path}.rect[${index}]`, `"${value}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`);
1463
+ add(
1464
+ `${path}.rect[${index}]`,
1465
+ `"${value}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`
1466
+ );
1366
1467
  }
1367
1468
  });
1368
1469
  }
@@ -1380,7 +1481,10 @@ function validateNode(node, path, layout, ctx) {
1380
1481
  );
1381
1482
  }
1382
1483
  if (key === "aspectRatio" && !checkAspectRatio(node.style[key])) {
1383
- add(`${path}.style.aspectRatio`, "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4");
1484
+ add(
1485
+ `${path}.style.aspectRatio`,
1486
+ "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
1487
+ );
1384
1488
  }
1385
1489
  });
1386
1490
  }
@@ -1410,7 +1514,8 @@ function validateNodeByType(node, path, ctx) {
1410
1514
  if (typeof node.bind !== "string") {
1411
1515
  add(`${path}.bind`, "tabs \u5FC5\u987B\u63D0\u4F9B bind\uFF08data \u4E2D\u7684\u6570\u7EC4\u5B57\u6BB5\u540D\uFF09");
1412
1516
  }
1413
- if (typeof node.stateKey !== "string") add(`${path}.stateKey`, "tabs \u5FC5\u987B\u63D0\u4F9B stateKey");
1517
+ if (typeof node.stateKey !== "string")
1518
+ add(`${path}.stateKey`, "tabs \u5FC5\u987B\u63D0\u4F9B stateKey");
1414
1519
  if (node.labelField !== void 0 && typeof node.labelField !== "string") {
1415
1520
  add(`${path}.labelField`, "labelField \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
1416
1521
  }
@@ -1476,7 +1581,10 @@ function validateAction(action, path, ctx) {
1476
1581
  add(`${path}.view`, `views \u91CC\u4E0D\u5B58\u5728\u89C6\u56FE "${action.view}"`);
1477
1582
  }
1478
1583
  if (action.mode !== void 0 && ["stack", "replace"].indexOf(action.mode) === -1) {
1479
- add(`${path}.mode`, "mode \u53EA\u80FD\u662F 'stack'\uFF08\u9ED8\u8BA4\uFF0C\u53E0\u4E00\u5C42\uFF09\u6216 'replace'\uFF08\u6362\u6389\u5F53\u524D\u5C42\uFF09");
1584
+ add(
1585
+ `${path}.mode`,
1586
+ "mode \u53EA\u80FD\u662F 'stack'\uFF08\u9ED8\u8BA4\uFF0C\u53E0\u4E00\u5C42\uFF09\u6216 'replace'\uFF08\u6362\u6389\u5F53\u524D\u5C42\uFF09"
1587
+ );
1480
1588
  }
1481
1589
  }
1482
1590
  if (action.type === "sequence") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoxiu/marketing-dsl",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "营销弹窗 DSL 解释器核心,纯逻辑无框架依赖",
6
6
  "license": "MIT",