@yaoxiu/marketing-dsl 1.1.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))
@@ -746,7 +760,12 @@ function resolveCloseButton(config, layerName, closeTop) {
746
760
  tag: "img",
747
761
  className: "dsl-close-img",
748
762
  src: image,
749
- style: { width: "100%", height: "100%", objectFit: "contain", display: "block" }
763
+ style: {
764
+ width: "100%",
765
+ height: "100%",
766
+ objectFit: "contain",
767
+ display: "block"
768
+ }
750
769
  }
751
770
  ]
752
771
  };
@@ -796,7 +815,14 @@ function resolveNode(node, key, layout, context, input, countdowns) {
796
815
  className: nodeClass("box", !!onClick),
797
816
  style: Object.assign({ position: "relative" }, style),
798
817
  onClick,
799
- 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
+ )
800
826
  };
801
827
  case "flex":
802
828
  return {
@@ -881,7 +907,14 @@ function resolveRepeat(node, key, context, input, countdowns) {
881
907
  [node.itemName || "item"]: item,
882
908
  [node.indexName || "index"]: index
883
909
  });
884
- 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
+ );
885
918
  }).filter((el) => !!el);
886
919
  }
887
920
  function resolveTabs(node, key, context, input) {
@@ -954,7 +987,14 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
954
987
  tag: "div",
955
988
  className: "dsl-node dsl-flex dsl-countdown",
956
989
  style: Object.assign({ display: "flex" }, style),
957
- 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
+ )
958
998
  };
959
999
  }
960
1000
  function toText(value) {
@@ -965,7 +1005,12 @@ function toText(value) {
965
1005
  var noopEmit = () => {
966
1006
  };
967
1007
  function createRuntime(dsl, options = {}) {
968
- 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
+ };
969
1014
  const normalized = normalizeViews(dsl);
970
1015
  let state = Object.assign({}, dsl.state || {});
971
1016
  let resolvedData = {};
@@ -981,7 +1026,10 @@ function createRuntime(dsl, options = {}) {
981
1026
  };
982
1027
  const ticker = createTicker({ onTick: notify });
983
1028
  function buildContext() {
984
- const context = Object.assign({}, resolvedData, { state, user });
1029
+ const context = Object.assign({}, resolvedData, {
1030
+ state,
1031
+ user: live.user
1032
+ });
985
1033
  const derived = dsl.derived || {};
986
1034
  Object.keys(derived).forEach((name) => {
987
1035
  const config = derived[name] || {};
@@ -1042,8 +1090,12 @@ function createRuntime(dsl, options = {}) {
1042
1090
  // 配置里 close 可以自带 reason(如 'never-remind'),要透传给埋点,不能吞掉
1043
1091
  closeTop: (reason) => closeTop(reason),
1044
1092
  closeAll: (reason) => closeAll(reason),
1045
- handlers,
1046
- editMode
1093
+ get handlers() {
1094
+ return live.handlers;
1095
+ },
1096
+ get editMode() {
1097
+ return live.editMode;
1098
+ }
1047
1099
  });
1048
1100
  function fireLifecycle(viewName, hook, extra) {
1049
1101
  const view = normalized.views[viewName];
@@ -1070,7 +1122,7 @@ function createRuntime(dsl, options = {}) {
1070
1122
  return;
1071
1123
  }
1072
1124
  pending.push(
1073
- Promise.resolve(source(ref.params || {}, user)).then((result) => {
1125
+ Promise.resolve(source(ref.params || {}, live.user)).then((result) => {
1074
1126
  resolved[key] = result;
1075
1127
  })
1076
1128
  );
@@ -1108,6 +1160,12 @@ function createRuntime(dsl, options = {}) {
1108
1160
  }
1109
1161
  return {
1110
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
+ },
1111
1169
  subscribe(listener) {
1112
1170
  listeners.add(listener);
1113
1171
  return () => listeners.delete(listener);
@@ -1186,7 +1244,10 @@ function validate(dsl) {
1186
1244
  }
1187
1245
  const doc = dsl;
1188
1246
  if (doc.version !== DSL_VERSION) {
1189
- 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
+ );
1190
1251
  }
1191
1252
  const dataKeys = validateData(doc.data, add);
1192
1253
  const stateKeys = validateState(doc.state, add);
@@ -1286,7 +1347,10 @@ function validateStage(stage, add, warn, ctx, prefix) {
1286
1347
  if (stage.onShow !== void 0) validateAction(stage.onShow, at("onShow"), ctx);
1287
1348
  if (stage.onClose !== void 0) validateAction(stage.onClose, at("onClose"), ctx);
1288
1349
  if (stage.height === "auto" && stage.layout !== "flow") {
1289
- 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
+ );
1290
1354
  }
1291
1355
  }
1292
1356
  function validateCloseButton(config, add, warn) {
@@ -1300,7 +1364,10 @@ function validateCloseButton(config, add, warn) {
1300
1364
  }
1301
1365
  if (config.offset !== void 0) {
1302
1366
  if (!Array.isArray(config.offset) || config.offset.length !== 2) {
1303
- 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
+ );
1304
1371
  } else {
1305
1372
  config.offset.forEach((value, index) => {
1306
1373
  if (!checkLength(value)) {
@@ -1321,7 +1388,10 @@ function validateCloseButton(config, add, warn) {
1321
1388
  if (config.style) {
1322
1389
  Object.keys(config.style).forEach((key) => {
1323
1390
  if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1324
- 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
+ );
1325
1395
  }
1326
1396
  });
1327
1397
  }
@@ -1392,7 +1462,10 @@ function validateNode(node, path, layout, ctx) {
1392
1462
  } else {
1393
1463
  node.rect.forEach((value, index) => {
1394
1464
  if (value !== void 0 && value !== null && !checkLength(value)) {
1395
- 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
+ );
1396
1469
  }
1397
1470
  });
1398
1471
  }
@@ -1410,7 +1483,10 @@ function validateNode(node, path, layout, ctx) {
1410
1483
  );
1411
1484
  }
1412
1485
  if (key === "aspectRatio" && !checkAspectRatio(node.style[key])) {
1413
- 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
+ );
1414
1490
  }
1415
1491
  });
1416
1492
  }
@@ -1440,7 +1516,8 @@ function validateNodeByType(node, path, ctx) {
1440
1516
  if (typeof node.bind !== "string") {
1441
1517
  add(`${path}.bind`, "tabs \u5FC5\u987B\u63D0\u4F9B bind\uFF08data \u4E2D\u7684\u6570\u7EC4\u5B57\u6BB5\u540D\uFF09");
1442
1518
  }
1443
- 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");
1444
1521
  if (node.labelField !== void 0 && typeof node.labelField !== "string") {
1445
1522
  add(`${path}.labelField`, "labelField \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
1446
1523
  }
@@ -1506,7 +1583,10 @@ function validateAction(action, path, ctx) {
1506
1583
  add(`${path}.view`, `views \u91CC\u4E0D\u5B58\u5728\u89C6\u56FE "${action.view}"`);
1507
1584
  }
1508
1585
  if (action.mode !== void 0 && ["stack", "replace"].indexOf(action.mode) === -1) {
1509
- 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
+ );
1510
1590
  }
1511
1591
  }
1512
1592
  if (action.type === "sequence") {
package/dist/index.d.cts CHANGED
@@ -293,6 +293,16 @@ interface RuntimeOptions {
293
293
  interface DslRuntime {
294
294
  /** 当前渲染树。每次 subscribe 回调后重新取 */
295
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;
296
306
  /** 状态变化时回调,返回取消订阅的函数 */
297
307
  subscribe: (listener: () => void) => () => void;
298
308
  /** 供调试台等外部读取,正常渲染不需要 */
package/dist/index.d.ts CHANGED
@@ -293,6 +293,16 @@ interface RuntimeOptions {
293
293
  interface DslRuntime {
294
294
  /** 当前渲染树。每次 subscribe 回调后重新取 */
295
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;
296
306
  /** 状态变化时回调,返回取消订阅的函数 */
297
307
  subscribe: (listener: () => void) => () => void;
298
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))
@@ -744,7 +758,12 @@ function resolveCloseButton(config, layerName, closeTop) {
744
758
  tag: "img",
745
759
  className: "dsl-close-img",
746
760
  src: image,
747
- style: { width: "100%", height: "100%", objectFit: "contain", display: "block" }
761
+ style: {
762
+ width: "100%",
763
+ height: "100%",
764
+ objectFit: "contain",
765
+ display: "block"
766
+ }
748
767
  }
749
768
  ]
750
769
  };
@@ -794,7 +813,14 @@ function resolveNode(node, key, layout, context, input, countdowns) {
794
813
  className: nodeClass("box", !!onClick),
795
814
  style: Object.assign({ position: "relative" }, style),
796
815
  onClick,
797
- 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
+ )
798
824
  };
799
825
  case "flex":
800
826
  return {
@@ -879,7 +905,14 @@ function resolveRepeat(node, key, context, input, countdowns) {
879
905
  [node.itemName || "item"]: item,
880
906
  [node.indexName || "index"]: index
881
907
  });
882
- 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
+ );
883
916
  }).filter((el) => !!el);
884
917
  }
885
918
  function resolveTabs(node, key, context, input) {
@@ -952,7 +985,14 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
952
985
  tag: "div",
953
986
  className: "dsl-node dsl-flex dsl-countdown",
954
987
  style: Object.assign({ display: "flex" }, style),
955
- 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
+ )
956
996
  };
957
997
  }
958
998
  function toText(value) {
@@ -963,7 +1003,12 @@ function toText(value) {
963
1003
  var noopEmit = () => {
964
1004
  };
965
1005
  function createRuntime(dsl, options = {}) {
966
- 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
+ };
967
1012
  const normalized = normalizeViews(dsl);
968
1013
  let state = Object.assign({}, dsl.state || {});
969
1014
  let resolvedData = {};
@@ -979,7 +1024,10 @@ function createRuntime(dsl, options = {}) {
979
1024
  };
980
1025
  const ticker = createTicker({ onTick: notify });
981
1026
  function buildContext() {
982
- const context = Object.assign({}, resolvedData, { state, user });
1027
+ const context = Object.assign({}, resolvedData, {
1028
+ state,
1029
+ user: live.user
1030
+ });
983
1031
  const derived = dsl.derived || {};
984
1032
  Object.keys(derived).forEach((name) => {
985
1033
  const config = derived[name] || {};
@@ -1040,8 +1088,12 @@ function createRuntime(dsl, options = {}) {
1040
1088
  // 配置里 close 可以自带 reason(如 'never-remind'),要透传给埋点,不能吞掉
1041
1089
  closeTop: (reason) => closeTop(reason),
1042
1090
  closeAll: (reason) => closeAll(reason),
1043
- handlers,
1044
- editMode
1091
+ get handlers() {
1092
+ return live.handlers;
1093
+ },
1094
+ get editMode() {
1095
+ return live.editMode;
1096
+ }
1045
1097
  });
1046
1098
  function fireLifecycle(viewName, hook, extra) {
1047
1099
  const view = normalized.views[viewName];
@@ -1068,7 +1120,7 @@ function createRuntime(dsl, options = {}) {
1068
1120
  return;
1069
1121
  }
1070
1122
  pending.push(
1071
- Promise.resolve(source(ref.params || {}, user)).then((result) => {
1123
+ Promise.resolve(source(ref.params || {}, live.user)).then((result) => {
1072
1124
  resolved[key] = result;
1073
1125
  })
1074
1126
  );
@@ -1106,6 +1158,12 @@ function createRuntime(dsl, options = {}) {
1106
1158
  }
1107
1159
  return {
1108
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
+ },
1109
1167
  subscribe(listener) {
1110
1168
  listeners.add(listener);
1111
1169
  return () => listeners.delete(listener);
@@ -1184,7 +1242,10 @@ function validate(dsl) {
1184
1242
  }
1185
1243
  const doc = dsl;
1186
1244
  if (doc.version !== DSL_VERSION) {
1187
- 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
+ );
1188
1249
  }
1189
1250
  const dataKeys = validateData(doc.data, add);
1190
1251
  const stateKeys = validateState(doc.state, add);
@@ -1284,7 +1345,10 @@ function validateStage(stage, add, warn, ctx, prefix) {
1284
1345
  if (stage.onShow !== void 0) validateAction(stage.onShow, at("onShow"), ctx);
1285
1346
  if (stage.onClose !== void 0) validateAction(stage.onClose, at("onClose"), ctx);
1286
1347
  if (stage.height === "auto" && stage.layout !== "flow") {
1287
- 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
+ );
1288
1352
  }
1289
1353
  }
1290
1354
  function validateCloseButton(config, add, warn) {
@@ -1298,7 +1362,10 @@ function validateCloseButton(config, add, warn) {
1298
1362
  }
1299
1363
  if (config.offset !== void 0) {
1300
1364
  if (!Array.isArray(config.offset) || config.offset.length !== 2) {
1301
- 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
+ );
1302
1369
  } else {
1303
1370
  config.offset.forEach((value, index) => {
1304
1371
  if (!checkLength(value)) {
@@ -1319,7 +1386,10 @@ function validateCloseButton(config, add, warn) {
1319
1386
  if (config.style) {
1320
1387
  Object.keys(config.style).forEach((key) => {
1321
1388
  if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1322
- 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
+ );
1323
1393
  }
1324
1394
  });
1325
1395
  }
@@ -1390,7 +1460,10 @@ function validateNode(node, path, layout, ctx) {
1390
1460
  } else {
1391
1461
  node.rect.forEach((value, index) => {
1392
1462
  if (value !== void 0 && value !== null && !checkLength(value)) {
1393
- 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
+ );
1394
1467
  }
1395
1468
  });
1396
1469
  }
@@ -1408,7 +1481,10 @@ function validateNode(node, path, layout, ctx) {
1408
1481
  );
1409
1482
  }
1410
1483
  if (key === "aspectRatio" && !checkAspectRatio(node.style[key])) {
1411
- 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
+ );
1412
1488
  }
1413
1489
  });
1414
1490
  }
@@ -1438,7 +1514,8 @@ function validateNodeByType(node, path, ctx) {
1438
1514
  if (typeof node.bind !== "string") {
1439
1515
  add(`${path}.bind`, "tabs \u5FC5\u987B\u63D0\u4F9B bind\uFF08data \u4E2D\u7684\u6570\u7EC4\u5B57\u6BB5\u540D\uFF09");
1440
1516
  }
1441
- 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");
1442
1519
  if (node.labelField !== void 0 && typeof node.labelField !== "string") {
1443
1520
  add(`${path}.labelField`, "labelField \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
1444
1521
  }
@@ -1504,7 +1581,10 @@ function validateAction(action, path, ctx) {
1504
1581
  add(`${path}.view`, `views \u91CC\u4E0D\u5B58\u5728\u89C6\u56FE "${action.view}"`);
1505
1582
  }
1506
1583
  if (action.mode !== void 0 && ["stack", "replace"].indexOf(action.mode) === -1) {
1507
- 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
+ );
1508
1588
  }
1509
1589
  }
1510
1590
  if (action.type === "sequence") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoxiu/marketing-dsl",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "营销弹窗 DSL 解释器核心,纯逻辑无框架依赖",
6
6
  "license": "MIT",