@weapp-vite/miniprogram-automator 1.2.13 → 1.2.14

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
@@ -107,6 +107,25 @@ await button?.tap()
107
107
  await page.waitFor(500)
108
108
  ```
109
109
 
110
+ ### 4.5 App-Service 降级(route fallback)能力边界
111
+
112
+ 部分 DevTools 版本(如 `2.01.2510290`)的 page-frame 定向协议失效,表现为 `Page.*` RPC 全部超时。此时本库自动切换到 App-Service 降级通道(`App.callFunction` + `wx.createSelectorQuery`),无需任何配置。
113
+
114
+ 降级模式下各能力可用性:
115
+
116
+ | 能力 | 状态 | 说明 |
117
+ | --------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------- |
118
+ | `page.$` / `page.$$` | ✅ | 经 route 查询返回降级元素 |
119
+ | `page.data()` / `page.setData()` / `page.callMethod()` | ✅ | 经 App-Service 读写页面数据、调用页面方法 |
120
+ | `element.offset()` / `element.size()` / `element.style()` | ✅ | 实时经 `createSelectorQuery` 读取 rect 与 computedStyle(每次读取都重新查询,不缓存) |
121
+ | `element.attribute('id')` / `element.attribute('data-*')` | ✅ | 经快照读取 id 与 dataset |
122
+ | `element.$()` / `element.$$()` | ❌ 明确报错 | 元素级查询依赖 page-frame 协议;请改用 `page.$()` / `page.$$()` |
123
+ | `element.text()` | ❌ 明确报错 | `SelectorQuery` 无法读取 `innerText`;请改用 `page.data()` 或 `page.callMethod()` 断言文案来源 |
124
+ | `element.tap()` / `trigger()` 等真实交互 | ❌ 明确报错 | AppService 无法合成真实触摸事件;请改用 `page.callMethod()` 或 `evaluate` 间接触发页面逻辑 |
125
+ | `scroll-view` 容器滚动 | ❌ | 需 page-frame 协议支持,降级模式不可用 |
126
+
127
+ > 降级元素上的不支持操作会**立即抛出带替代建议的错误**,不会再像旧版那样挂起 30s 才超时。
128
+
110
129
  ## 5. 主要导出
111
130
 
112
131
  | 导出 | 说明 |
package/dist/index.d.mts CHANGED
@@ -94,6 +94,12 @@ interface IElementOptions {
94
94
  videoId?: string;
95
95
  pageId: number;
96
96
  routeFallback?: boolean;
97
+ /** route 降级元素的定位信息:用于只读方法经 App-service 实时重查快照。 */
98
+ routeFallbackSelector?: string;
99
+ routeFallbackIndex?: number;
100
+ routeFallbackRoute?: string;
101
+ routeFallbackQuery?: Record<string, any>;
102
+ routeFallbackScopeSelectors?: string[];
97
103
  tagName: string;
98
104
  }
99
105
  /** ITouch 的类型定义。 */
@@ -123,7 +129,7 @@ interface ElementQueryOptions {
123
129
  }
124
130
  /** Element 的实现。 */
125
131
  declare class Element {
126
- private connection;
132
+ protected readonly connection: Connection;
127
133
  private elementMap;
128
134
  tagName: string;
129
135
  protected nodeId: string | null;
package/dist/index.mjs CHANGED
@@ -222,6 +222,223 @@ function toStr(value) {
222
222
  return value == null ? "" : String(value);
223
223
  }
224
224
  //#endregion
225
+ //#region src/pageRouteSnapshot.ts
226
+ const ROUTE_ELEMENT_SNAPSHOT_TIMEOUT = 5e3;
227
+ const ROUTE_ELEMENT_SNAPSHOT_EMPTY_RETRIES = 2;
228
+ const ROUTE_ELEMENT_SNAPSHOT_RETRY_DELAY = 220;
229
+ /**
230
+ * 页面定位 + 组件作用域遍历逻辑与 Page.renderedNodes 保持一致:
231
+ * 先按 route/query 找到目标页面,再逐层进入自定义组件作用域查询,
232
+ * 返回首个命中作用域里 selectAll(selector)[index] 的快照;未命中返回 null。
233
+ */
234
+ const ROUTE_ELEMENT_SNAPSHOT_FUNCTION_DECLARATION = `function (route, query, selector, index, scopeSelectors, styleNames) {
235
+ function normalizeRoute(value) {
236
+ return String(value || '').replace(/^\\/+/, '').replace(/\\/+$/g, '');
237
+ }
238
+ function matchesQuery(page, expectedQuery) {
239
+ if (!expectedQuery || !Object.keys(expectedQuery).length) {
240
+ return true;
241
+ }
242
+ var actualQuery = page && (page.options || page.query || {});
243
+ return Object.keys(expectedQuery).every(function (key) {
244
+ var actualValue = String(actualQuery[key] == null ? '' : actualQuery[key]);
245
+ var expectedValue = String(expectedQuery[key]);
246
+ var decodedActualValue = actualValue;
247
+ try {
248
+ decodedActualValue = decodeURIComponent(actualValue);
249
+ }
250
+ catch (_) {
251
+ }
252
+ return actualValue === expectedValue
253
+ || decodedActualValue === expectedValue
254
+ || actualValue === encodeURIComponent(expectedValue);
255
+ });
256
+ }
257
+ var pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [];
258
+ var expectedRoute = normalizeRoute(route);
259
+ var page = null;
260
+ for (var pageIndex = pages.length - 1; pageIndex >= 0; pageIndex -= 1) {
261
+ var candidate = pages[pageIndex];
262
+ var candidateRoute = normalizeRoute(candidate && (candidate.path || candidate.route || candidate.__route__));
263
+ if ((!expectedRoute || candidateRoute === expectedRoute) && matchesQuery(candidate, query)) {
264
+ page = candidate;
265
+ break;
266
+ }
267
+ }
268
+ if (!page) {
269
+ return null;
270
+ }
271
+ function createSelectorQuery(scope) {
272
+ var selectorQuery = null;
273
+ var createdFromScope = false;
274
+ if (scope && typeof scope.createSelectorQuery === 'function') {
275
+ selectorQuery = scope.createSelectorQuery();
276
+ createdFromScope = true;
277
+ }
278
+ else if (typeof wx !== 'undefined' && wx && typeof wx.createSelectorQuery === 'function') {
279
+ selectorQuery = wx.createSelectorQuery();
280
+ }
281
+ else if (typeof page.createSelectorQuery === 'function') {
282
+ selectorQuery = page.createSelectorQuery();
283
+ }
284
+ if (!selectorQuery) {
285
+ return null;
286
+ }
287
+ if (!createdFromScope && typeof selectorQuery.in === 'function') {
288
+ try {
289
+ selectorQuery = selectorQuery.in(scope || page);
290
+ }
291
+ catch (_) {
292
+ }
293
+ }
294
+ return selectorQuery;
295
+ }
296
+ function pushUnique(list, seen, item) {
297
+ if (!item) {
298
+ return;
299
+ }
300
+ var id = item.is || item.id || item.__wxWebviewId__ || item.__wxExparserNodeId__ || String(list.length);
301
+ if (seen[id]) {
302
+ return;
303
+ }
304
+ seen[id] = true;
305
+ list.push(item);
306
+ }
307
+ function collectScopes(root, scopeSelectors) {
308
+ var scopes = [];
309
+ var seen = {};
310
+ var queue = [];
311
+ pushUnique(scopes, seen, root);
312
+ queue.push(root);
313
+ for (var queueIndex = 0; queueIndex < queue.length && queueIndex < 60; queueIndex += 1) {
314
+ var scope = queue[queueIndex];
315
+ if (!scope || typeof scope.selectAllComponents !== 'function') {
316
+ continue;
317
+ }
318
+ var children = [];
319
+ var componentSelectors = Array.isArray(scopeSelectors) ? scopeSelectors.slice() : [];
320
+ componentSelectors.push(selector, '*', 'weapp-app-shell', 'weapp-layout-default');
321
+ for (var selectorIndex = 0; selectorIndex < componentSelectors.length; selectorIndex += 1) {
322
+ if (!componentSelectors[selectorIndex]) {
323
+ continue;
324
+ }
325
+ try {
326
+ var selected = scope.selectAllComponents(componentSelectors[selectorIndex]);
327
+ if (Array.isArray(selected)) {
328
+ children = children.concat(selected);
329
+ }
330
+ }
331
+ catch (_) {
332
+ }
333
+ }
334
+ for (var childIndex = 0; childIndex < children.length; childIndex += 1) {
335
+ var child = children[childIndex];
336
+ var previousLength = scopes.length;
337
+ pushUnique(scopes, seen, child);
338
+ if (scopes.length > previousLength) {
339
+ queue.push(child);
340
+ }
341
+ }
342
+ }
343
+ return scopes;
344
+ }
345
+ function queryScope(scope) {
346
+ return new Promise(function (resolve) {
347
+ var settled = false;
348
+ var timer = setTimeout(function () {
349
+ if (settled) {
350
+ return;
351
+ }
352
+ settled = true;
353
+ resolve([]);
354
+ }, 800);
355
+ function finish(nodes) {
356
+ if (settled) {
357
+ return;
358
+ }
359
+ settled = true;
360
+ clearTimeout(timer);
361
+ resolve(Array.isArray(nodes) ? nodes : nodes ? [nodes] : []);
362
+ }
363
+ try {
364
+ var selectorQuery = createSelectorQuery(scope);
365
+ if (!selectorQuery) {
366
+ finish([]);
367
+ return;
368
+ }
369
+ selectorQuery
370
+ .selectAll(selector)
371
+ .fields({
372
+ id: true,
373
+ dataset: true,
374
+ rect: true,
375
+ size: true,
376
+ computedStyle: Array.isArray(styleNames) ? styleNames : []
377
+ }, function (nodes) {
378
+ finish(nodes);
379
+ })
380
+ .exec();
381
+ }
382
+ catch (_) {
383
+ finish([]);
384
+ }
385
+ });
386
+ }
387
+ return new Promise(function (resolve) {
388
+ try {
389
+ var scopes = collectScopes(page, scopeSelectors);
390
+ var scopeIndex = 0;
391
+ function next() {
392
+ if (scopeIndex >= scopes.length) {
393
+ resolve(null);
394
+ return;
395
+ }
396
+ var scope = scopes[scopeIndex];
397
+ scopeIndex += 1;
398
+ queryScope(scope).then(function (nodes) {
399
+ if (nodes.length > 0) {
400
+ resolve(nodes[index] || null);
401
+ return;
402
+ }
403
+ next();
404
+ }, function () {
405
+ next();
406
+ });
407
+ }
408
+ next();
409
+ }
410
+ catch (_) {
411
+ resolve(null);
412
+ }
413
+ });
414
+ }`;
415
+ /**
416
+ * 读取降级元素快照。未找到(页面不在/选择器无命中/索引越界)返回 null,
417
+ * 由调用方决定抛错文案;永不因为协议失效而长时间挂起。
418
+ */
419
+ async function readRouteElementSnapshot(connection, route, query, selector, index, scopeSelectors = [], styleNames = [], timeout = ROUTE_ELEMENT_SNAPSHOT_TIMEOUT) {
420
+ const startedAt = Date.now();
421
+ for (let attempt = 0; attempt <= ROUTE_ELEMENT_SNAPSHOT_EMPTY_RETRIES; attempt += 1) {
422
+ const remaining = attempt === 0 ? timeout : Math.max(1, timeout - (Date.now() - startedAt));
423
+ const { result } = await connection.send("App.callFunction", {
424
+ functionDeclaration: ROUTE_ELEMENT_SNAPSHOT_FUNCTION_DECLARATION,
425
+ args: [
426
+ route,
427
+ query,
428
+ selector,
429
+ index,
430
+ scopeSelectors,
431
+ styleNames
432
+ ]
433
+ }, { timeout: remaining });
434
+ if (result != null || attempt === ROUTE_ELEMENT_SNAPSHOT_EMPTY_RETRIES) return result ?? null;
435
+ const retryRemaining = timeout - (Date.now() - startedAt);
436
+ if (retryRemaining <= 0) break;
437
+ await sleep$1(Math.min(ROUTE_ELEMENT_SNAPSHOT_RETRY_DELAY, retryRemaining));
438
+ }
439
+ return null;
440
+ }
441
+ //#endregion
225
442
  //#region src/Element.ts
226
443
  const ELEMENT_QUERY_TIMEOUT = 2500;
227
444
  const ELEMENT_WXML_TIMEOUT = 8e3;
@@ -272,7 +489,17 @@ var Element = class Element {
272
489
  };
273
490
  }
274
491
  async offset() {
275
- return await this.send("Element.getOffset");
492
+ const offset = await this.send("Element.getOffset");
493
+ if (offset?.width !== void 0 && offset.height !== void 0) return offset;
494
+ try {
495
+ const dimensions = await this.size();
496
+ return {
497
+ ...offset,
498
+ ...dimensions
499
+ };
500
+ } catch {
501
+ return offset;
502
+ }
276
503
  }
277
504
  async text() {
278
505
  return await this.domProperty("innerText");
@@ -394,8 +621,87 @@ var Element = class Element {
394
621
  };
395
622
  /** App-Service 路由降级元素仅用于只读查询,不能伪装成交互协议元素。 */
396
623
  var RouteFallbackElement = class extends Element {
624
+ routeSelector;
625
+ routeIndex;
626
+ routeRoute;
627
+ routeQuery;
628
+ routeScopeSelectors;
629
+ constructor(connection, options, elementMap) {
630
+ super(connection, options, elementMap);
631
+ this.routeSelector = options.routeFallbackSelector;
632
+ this.routeIndex = options.routeFallbackIndex;
633
+ this.routeRoute = options.routeFallbackRoute;
634
+ this.routeQuery = options.routeFallbackQuery;
635
+ this.routeScopeSelectors = options.routeFallbackScopeSelectors ?? [];
636
+ }
637
+ /**
638
+ * 经 App-service SelectorQuery 实时重查元素快照(rect/size/dataset/computedStyle)。
639
+ * 每次读取都重新查询,保证滚动/重渲染后拿到的是当前值;未命中返回 null。
640
+ */
641
+ async readSnapshot(styleNames = []) {
642
+ if (!this.routeSelector || this.routeIndex === void 0 || !this.routeRoute) return null;
643
+ return await readRouteElementSnapshot(this.connection, this.routeRoute, this.routeQuery ?? {}, this.routeSelector, this.routeIndex, this.routeScopeSelectors, styleNames);
644
+ }
645
+ missingSnapshot(what) {
646
+ return /* @__PURE__ */ new Error(`route fallback 元素未取到${what}: ${this.routeSelector ?? "(未知选择器)"}(页面已切换或元素已卸载)`);
647
+ }
648
+ async $(selector, _options = {}) {
649
+ return this.unsupported(`Element.$(${selector})`);
650
+ }
651
+ async $$(selector, _options = {}) {
652
+ return this.unsupported(`Element.$$(${selector})`);
653
+ }
654
+ async offset() {
655
+ const snapshot = await this.readSnapshot();
656
+ if (snapshot?.left === void 0 || snapshot.top === void 0) throw this.missingSnapshot("坐标");
657
+ return {
658
+ left: snapshot.left,
659
+ top: snapshot.top,
660
+ width: snapshot.width,
661
+ height: snapshot.height
662
+ };
663
+ }
664
+ async size() {
665
+ const snapshot = await this.readSnapshot();
666
+ if (snapshot?.width === void 0 || snapshot.height === void 0) throw this.missingSnapshot("尺寸");
667
+ return {
668
+ width: snapshot.width,
669
+ height: snapshot.height
670
+ };
671
+ }
672
+ async style(name) {
673
+ if (!isStr(name)) throw new Error("name must be a string");
674
+ const value = (await this.readSnapshot([name]))?.[name];
675
+ if (value === void 0) throw this.missingSnapshot(`样式 ${name}`);
676
+ return value;
677
+ }
678
+ async attribute(name) {
679
+ if (!isStr(name)) throw new Error("name must be a string");
680
+ const snapshot = await this.readSnapshot();
681
+ if (name === "id") return snapshot?.id;
682
+ if (name.startsWith("data-")) {
683
+ const key = name.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
684
+ return snapshot?.dataset?.[key];
685
+ }
686
+ throw new Error(`route fallback 元素仅支持读取 id 与 data-* 属性,不支持 attribute(${name})`);
687
+ }
397
688
  unsupported(method) {
398
- throw new Error(`App-Service route fallback 元素不支持 ${method}`);
689
+ throw new Error(`App-Service route fallback 元素不支持 ${method}(真实交互依赖已失效的 page-frame 协议);可改用 page.callMethod()/evaluate 间接触发页面逻辑`);
690
+ }
691
+ async text() {
692
+ throw new Error("App-Service route fallback 元素不支持 text(SelectorQuery 无法读取 innerText);请改用 page.data() 断言数据源,或 page.callMethod()/evaluate 读取");
693
+ }
694
+ async value() {
695
+ this.unsupported("Element.value");
696
+ }
697
+ async property(name) {
698
+ this.unsupported(`Element.property(${name})`);
699
+ }
700
+ async wxml() {
701
+ this.unsupported("Element.wxml");
702
+ }
703
+ async outerWxml() {
704
+ this.unsupported("Element.outerWxml");
399
705
  }
400
706
  async tap() {
401
707
  this.unsupported("Element.tap");
@@ -640,7 +946,7 @@ var Connection = class Connection extends EventEmitter {
640
946
  //#endregion
641
947
  //#region src/headless.ts
642
948
  async function launchHeadlessAutomator(options) {
643
- return await (await import("./launch-yIGimB0F.mjs")).launch({ projectPath: options.projectPath });
949
+ return await (await import("./launch-DKcPk1N2.mjs")).launch({ projectPath: options.projectPath });
644
950
  }
645
951
  //#endregion
646
952
  //#region src/launcher/portLease.ts
@@ -739,7 +1045,7 @@ async function acquireAutomatorPortLease(preferredPort) {
739
1045
  }
740
1046
  //#endregion
741
1047
  //#region package.json
742
- var version = "1.2.13";
1048
+ var version = "1.2.14";
743
1049
  //#endregion
744
1050
  //#region src/Native.ts
745
1051
  /** Native 的实现。 */
@@ -800,14 +1106,19 @@ function resolveFallbackTagName(selector) {
800
1106
  return "view";
801
1107
  }
802
1108
  /** 将 App-Service 查询到的渲染节点包装为 Element 兼容对象。 */
803
- function createRouteFallbackElement(connection, elementMap, pageId, selector, node, index) {
1109
+ function createRouteFallbackElement(connection, elementMap, pageId, selector, node, index, route, query, scopeSelectors = []) {
804
1110
  const nodeId = typeof node.id === "string" && node.id ? normalizeFallbackIdPart(node.id) : String(index);
805
1111
  const elementId = `__weapp_vite_route_fallback_${pageId}_${normalizeFallbackIdPart(selector)}_${nodeId}_${index}`;
806
1112
  return Element.create(connection, {
807
1113
  elementId,
808
1114
  pageId,
809
1115
  routeFallback: true,
810
- tagName: resolveFallbackTagName(selector)
1116
+ tagName: resolveFallbackTagName(selector),
1117
+ routeFallbackSelector: selector,
1118
+ routeFallbackIndex: index,
1119
+ routeFallbackRoute: route,
1120
+ routeFallbackQuery: query,
1121
+ routeFallbackScopeSelectors: [...scopeSelectors]
811
1122
  }, elementMap);
812
1123
  }
813
1124
  //#endregion
@@ -976,6 +1287,7 @@ const PAGE_CALL_METHOD_FALLBACK_RETRY_DELAY = 300;
976
1287
  const PAGE_RENDERED_TIMEOUT = 15e3;
977
1288
  const PAGE_RENDERED_POLL_DELAY = 220;
978
1289
  const PAGE_RENDERED_QUERY_TIMEOUT = 5e3;
1290
+ const PAGE_RENDERED_EMPTY_RETRIES = 2;
979
1291
  const PAGE_ROOT_SELECTORS = [
980
1292
  "page",
981
1293
  "body",
@@ -1723,10 +2035,21 @@ var Page = class Page {
1723
2035
  return (await this.queryRouteElements(selector, options))[0] ?? null;
1724
2036
  }
1725
2037
  async queryRouteElements(selector, options) {
1726
- return (await this.renderedNodes(selector, {
1727
- componentSelectors: options.componentSelectors,
1728
- timeout: options.timeout ?? PAGE_QUERY_TIMEOUT
1729
- })).map((node, index) => createRouteFallbackElement(this.connection, this.elementMap, this.id, selector, node, index));
2038
+ const timeout = options.timeout ?? PAGE_QUERY_TIMEOUT;
2039
+ const startedAt = Date.now();
2040
+ let nodes = [];
2041
+ for (let attempt = 0; attempt <= PAGE_RENDERED_EMPTY_RETRIES; attempt += 1) {
2042
+ const requestTimeout = attempt === 0 ? timeout : Math.max(1, timeout - (Date.now() - startedAt));
2043
+ nodes = await this.renderedNodes(selector, {
2044
+ componentSelectors: options.componentSelectors,
2045
+ timeout: requestTimeout
2046
+ });
2047
+ if (nodes.length > 0 || attempt === PAGE_RENDERED_EMPTY_RETRIES) break;
2048
+ const remaining = timeout - (Date.now() - startedAt);
2049
+ if (remaining <= 0) break;
2050
+ await sleep$1(Math.min(PAGE_RENDERED_POLL_DELAY, remaining));
2051
+ }
2052
+ return nodes.map((node, index) => createRouteFallbackElement(this.connection, this.elementMap, this.id, selector, node, index, this.path, this.query, options.componentSelectors));
1730
2053
  }
1731
2054
  async callRouteMethod(method, args, timeout = PAGE_CALL_METHOD_TIMEOUT) {
1732
2055
  return await callRouteMethodViaAppService(this.connection, this.path, this.query, method, args, timeout);
@@ -14854,9 +14854,10 @@ function resolveComponentProperties(clonedNode, scope, definition) {
14854
14854
  };
14855
14855
  }
14856
14856
  function createRuntimeComponentInstance(componentScopeId, context, clonedNode, componentEntry, nextProperties, ownerScopeId) {
14857
+ const componentProperties = Object.keys(componentEntry.definition.methods ?? {}).some((key) => key.startsWith("__weapp_vite_")) || Object.hasOwn(componentEntry.definition.properties ?? {}, "__wvSlotOwnerId") ? Object.fromEntries(Object.keys(componentEntry.definition.properties ?? {}).filter((key) => Object.hasOwn(nextProperties, key)).map((key) => [key, nextProperties[key]])) : nextProperties;
14857
14858
  const componentInstance = createComponentInstance({
14858
14859
  definition: componentEntry.definition,
14859
- properties: nextProperties,
14860
+ properties: componentProperties,
14860
14861
  requestRender: (callback) => context.session.requestRender(callback),
14861
14862
  triggerEvent: buildComponentTrigger(componentScopeId, context, clonedNode)
14862
14863
  });
@@ -14869,7 +14870,7 @@ function createRuntimeComponentInstance(componentScopeId, context, clonedNode, c
14869
14870
  componentInstance.selectOwnerComponent = () => ownerScopeId ? context.componentCache.get(ownerScopeId) ?? null : null;
14870
14871
  context.componentCache.set(componentScopeId, componentInstance);
14871
14872
  runComponentLifecycle(componentInstance, "created");
14872
- runComponentObservers(componentInstance.__definition__ ?? componentEntry.definition, componentInstance, Object.keys(nextProperties), {});
14873
+ runComponentObservers(componentInstance.__definition__ ?? componentEntry.definition, componentInstance, Object.keys(componentProperties), {});
14873
14874
  componentInstance.__propertySnapshots = Object.fromEntries(Object.entries(componentInstance.properties).map(([key, propertyValue]) => [key, cloneValue$1(propertyValue)]));
14874
14875
  runComponentLifecycle(componentInstance, "attached");
14875
14876
  return componentInstance;
@@ -15138,15 +15139,21 @@ function registerAppDefinition(registries, definition) {
15138
15139
  registries.appDefinition = definition;
15139
15140
  return definition;
15140
15141
  }
15141
- function registerPageDefinition(registries, definition) {
15142
- const route = registries.currentLoadContext?.route;
15142
+ function registerPageDefinition(registries, definition, explicitRoute) {
15143
+ const route = explicitRoute ?? registries.currentLoadContext?.route;
15143
15144
  if (!route) throw new Error("Cannot register Page() without an active page load context in headless runtime.");
15144
15145
  if (registries.pages.has(route)) throw createDuplicateRegistrationError("page", route);
15145
15146
  registries.pages.set(route, definition);
15146
15147
  return definition;
15147
15148
  }
15148
15149
  function registerComponentDefinition(registries, definition) {
15149
- if (registries.currentLoadContext?.kind === "page") return registerPageDefinition(registries, normalizeComponentPageDefinition(definition));
15150
+ if (registries.currentLoadContext?.kind === "page") {
15151
+ if (registries.currentLoadContext.componentDefinitions) {
15152
+ registries.currentLoadContext.componentDefinitions.push(definition);
15153
+ return definition;
15154
+ }
15155
+ return registerPageDefinition(registries, normalizeComponentPageDefinition(definition));
15156
+ }
15150
15157
  const id = registries.currentLoadContext?.route ?? `anonymous:${registries.components.size}`;
15151
15158
  if (registries.components.has(id)) throw createDuplicateRegistrationError("component", id);
15152
15159
  registries.components.set(id, definition);
@@ -15694,15 +15701,24 @@ var RuntimeDiagnostics = class {
15694
15701
  timestamp: Date.now()
15695
15702
  });
15696
15703
  }
15697
- createConsole(baseConsole = console) {
15704
+ createConsole(baseConsole = console, onConsole) {
15698
15705
  const runtimeConsole = Object.create(baseConsole);
15706
+ const wrap = (level, method) => (...args) => {
15707
+ if (level === "error" || level === "warn") this.record(level, args);
15708
+ onConsole?.({
15709
+ args: [...args],
15710
+ level
15711
+ });
15712
+ method(...args);
15713
+ };
15714
+ runtimeConsole.debug = wrap("debug", baseConsole.debug.bind(baseConsole));
15715
+ runtimeConsole.info = wrap("info", baseConsole.info.bind(baseConsole));
15716
+ runtimeConsole.log = wrap("log", baseConsole.log.bind(baseConsole));
15699
15717
  runtimeConsole.error = (...args) => {
15700
- this.record("error", args);
15701
- baseConsole.error(...args);
15718
+ wrap("error", baseConsole.error.bind(baseConsole))(...args);
15702
15719
  };
15703
15720
  runtimeConsole.warn = (...args) => {
15704
- this.record("warn", args);
15705
- baseConsole.warn(...args);
15721
+ wrap("warn", baseConsole.warn.bind(baseConsole))(...args);
15706
15722
  };
15707
15723
  return runtimeConsole;
15708
15724
  }
@@ -16321,12 +16337,15 @@ function parseNumericLikeValue$1(value) {
16321
16337
  const match = value.match(NUMERIC_LIKE_VALUE_RE$1);
16322
16338
  return match ? Number(match[0]) : 0;
16323
16339
  }
16324
- function resolveRect$1(node) {
16340
+ function resolveRect$1(node, windowInfo) {
16325
16341
  const style = parseStyleDeclarations$1(node.attribs?.style);
16326
16342
  const left = parseNumericLikeValue$1(node.attribs?.["data-sim-left"] ?? style.left);
16327
16343
  const top = parseNumericLikeValue$1(node.attribs?.["data-sim-top"] ?? style.top);
16328
- const width = parseNumericLikeValue$1(node.attribs?.["data-sim-width"] ?? style.width);
16329
- const height = parseNumericLikeValue$1(node.attribs?.["data-sim-height"] ?? style.height);
16344
+ const rawWidth = node.attribs?.["data-sim-width"] ?? style.width;
16345
+ const rawHeight = node.attribs?.["data-sim-height"] ?? style.height;
16346
+ const isTopLevelView = node.name === "view" && node.parent?.name === "page" && windowInfo?.windowWidth != null;
16347
+ const width = rawWidth != null ? parseNumericLikeValue$1(rawWidth) : isTopLevelView && windowInfo ? windowInfo.windowWidth : 0;
16348
+ const height = rawHeight != null ? parseNumericLikeValue$1(rawHeight) : isTopLevelView && windowInfo ? windowInfo.windowHeight : 0;
16330
16349
  return {
16331
16350
  bottom: top + height,
16332
16351
  height,
@@ -16373,7 +16392,7 @@ function resolveFieldsResult(node, fields, options) {
16373
16392
  if (fields.dataset) result.dataset = collectDataset$1(node);
16374
16393
  if (fields.mark) result.mark = collectMark(node);
16375
16394
  if (fields.rect || fields.size) {
16376
- const rect = resolveRect$1(node);
16395
+ const rect = resolveRect$1(node, options.root.name === "page" ? options.windowInfo : void 0);
16377
16396
  if (fields.rect) Object.assign(result, rect);
16378
16397
  if (fields.size) {
16379
16398
  result.width = rect.width;
@@ -17161,6 +17180,102 @@ function createHeadlessVideoContext(driver, videoId, scope) {
17161
17180
  }
17162
17181
  //#endregion
17163
17182
  //#region ../../mpcore/packages/simulator/src/runtime/moduleLoader.ts
17183
+ const ESM_IMPORT_LINE_RE = /^[ \t]*import[^\n]*$/gm;
17184
+ const ESM_EXPORT_LIST_RE = /^[ \t]*export[ \t]*\{([^}\n]+)\}[ \t]*;?$/gm;
17185
+ const ESM_EXPORT_DECLARATION_RE = /\bexport\s+(const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/g;
17186
+ const ESM_EXPORT_DEFAULT_DECLARATION_RE = /\bexport\s+default\s+(function|class)\s+([A-Za-z_$][\w$]*)/g;
17187
+ function isImportWhitespace(value) {
17188
+ return value === " " || value === " ";
17189
+ }
17190
+ function parseEsmImportLine(line) {
17191
+ let statement = line.trim().slice(6).trim();
17192
+ if (statement.endsWith(";")) statement = statement.slice(0, -1).trimEnd();
17193
+ if (statement.startsWith("\"") || statement.startsWith("'")) {
17194
+ const quote = statement[0];
17195
+ if (statement.endsWith(quote) && statement.length > 1) return {
17196
+ request: statement.slice(1, -1),
17197
+ sideEffect: true
17198
+ };
17199
+ return;
17200
+ }
17201
+ let fromIndex = statement.indexOf("from");
17202
+ while (fromIndex >= 0) {
17203
+ if (isImportWhitespace(statement[fromIndex - 1]) && isImportWhitespace(statement[fromIndex + 4])) break;
17204
+ fromIndex = statement.indexOf("from", fromIndex + 4);
17205
+ }
17206
+ if (fromIndex < 0) return;
17207
+ const specifier = statement.slice(0, fromIndex).trim();
17208
+ const requestLiteral = statement.slice(fromIndex + 4).trim();
17209
+ const quote = requestLiteral[0];
17210
+ if (!specifier || quote !== "\"" && quote !== "'" || requestLiteral.length < 2 || !requestLiteral.endsWith(quote)) return;
17211
+ return {
17212
+ request: requestLiteral.slice(1, -1),
17213
+ sideEffect: false,
17214
+ specifier
17215
+ };
17216
+ }
17217
+ function createEsmImportBindings(specifier, request, index) {
17218
+ const namespaceName = `__headlessEsmImport${index}`;
17219
+ const statements = [`const ${namespaceName} = require(${JSON.stringify(request)});`];
17220
+ const normalizedSpecifier = specifier.trim();
17221
+ if (normalizedSpecifier.startsWith("{") && normalizedSpecifier.endsWith("}")) {
17222
+ const entries = normalizedSpecifier.slice(1, -1).split(",");
17223
+ for (const entry of entries) {
17224
+ const [importedName, localName] = entry.trim().split(/\s+as\s+/);
17225
+ if (!importedName) continue;
17226
+ const bindingName = localName || importedName;
17227
+ statements.push(`const ${bindingName} = ${namespaceName}[${JSON.stringify(importedName)}];`);
17228
+ }
17229
+ return statements.join("\n");
17230
+ }
17231
+ if (normalizedSpecifier.startsWith("* as ")) {
17232
+ const bindingName = normalizedSpecifier.slice(5).trim();
17233
+ statements.push(`const ${bindingName} = ${namespaceName};`);
17234
+ return statements.join("\n");
17235
+ }
17236
+ const [defaultName, namedPart] = normalizedSpecifier.split(/\s*,\s*/, 2);
17237
+ if (defaultName) statements.push(`const ${defaultName.trim()} = ${namespaceName}.default ?? ${namespaceName};`);
17238
+ if (namedPart?.startsWith("{") && namedPart.endsWith("}")) statements.push(createEsmImportBindings(namedPart, request, index).split("\n").slice(1).join("\n"));
17239
+ return statements.join("\n");
17240
+ }
17241
+ function transformEsmToCommonJs(source) {
17242
+ if (!/^\s*(?:import|export)\b/m.test(source)) return source;
17243
+ let importIndex = 0;
17244
+ const exportedNames = [];
17245
+ let transformed = source.replace(ESM_IMPORT_LINE_RE, (line) => {
17246
+ const parsed = parseEsmImportLine(line);
17247
+ if (!parsed) return line;
17248
+ if (parsed.sideEffect) return `require(${JSON.stringify(parsed.request)});`;
17249
+ return createEsmImportBindings(parsed.specifier, parsed.request, importIndex++);
17250
+ });
17251
+ transformed = transformed.replace(ESM_EXPORT_LIST_RE, (_match, entries) => {
17252
+ for (const entry of entries.split(",")) {
17253
+ const [local, exported] = entry.trim().split(/\s+as\s+/);
17254
+ if (local) exportedNames.push({
17255
+ exported: exported || local,
17256
+ local
17257
+ });
17258
+ }
17259
+ return "";
17260
+ });
17261
+ transformed = transformed.replace(ESM_EXPORT_DEFAULT_DECLARATION_RE, (_match, kind, local) => {
17262
+ exportedNames.push({
17263
+ exported: "default",
17264
+ local
17265
+ });
17266
+ return `${kind} ${local}`;
17267
+ });
17268
+ transformed = transformed.replace(ESM_EXPORT_DECLARATION_RE, (_match, kind, local) => {
17269
+ exportedNames.push({
17270
+ exported: local,
17271
+ local
17272
+ });
17273
+ return `${kind} ${local}`;
17274
+ });
17275
+ transformed = transformed.replace(/\bexport\s+default\s+/g, "module.exports.default = ");
17276
+ if (exportedNames.length > 0) transformed += `\n${exportedNames.map(({ exported, local }) => `module.exports[${JSON.stringify(exported)}] = ${local};`).join("\n")}`;
17277
+ return transformed;
17278
+ }
17164
17279
  function createRequireNotFoundError(request, importer) {
17165
17280
  return /* @__PURE__ */ new Error(`Cannot resolve require("${request}") from ${normalize(importer)} in headless runtime.`);
17166
17281
  }
@@ -17176,7 +17291,7 @@ function resolveRequiredModulePath(artifactSource, importer, request) {
17176
17291
  for (const candidate of candidates) if (artifactSource.has(candidate)) return candidate;
17177
17292
  throw createRequireNotFoundError(request, importer);
17178
17293
  }
17179
- function createExecutionContext(registries, getCurrentPages, getApp, wxDriver, kernel, globals) {
17294
+ function createExecutionContext(registries, getCurrentPages, getApp, wxDriver, kernel, globals, onConsole) {
17180
17295
  const wx = createHeadlessWx(wxDriver);
17181
17296
  return {
17182
17297
  App(definition) {
@@ -17194,7 +17309,7 @@ function createExecutionContext(registries, getCurrentPages, getApp, wxDriver, k
17194
17309
  Page(definition) {
17195
17310
  return registerPageDefinition(registries, definition);
17196
17311
  },
17197
- console: kernel.diagnostics.createConsole(),
17312
+ console: kernel.diagnostics.createConsole(console, onConsole),
17198
17313
  clearInterval: (handle) => kernel.scheduler.clearInterval(handle),
17199
17314
  clearTimeout: (handle) => kernel.scheduler.clearTimeout(handle),
17200
17315
  getApp,
@@ -17204,13 +17319,17 @@ function createExecutionContext(registries, getCurrentPages, getApp, wxDriver, k
17204
17319
  require: void 0,
17205
17320
  setInterval: kernel.scheduler.setInterval.bind(kernel.scheduler),
17206
17321
  setTimeout: kernel.scheduler.setTimeout.bind(kernel.scheduler),
17322
+ TextDecoder: globalThis.TextDecoder,
17323
+ TextEncoder: globalThis.TextEncoder,
17324
+ URL: globalThis.URL,
17325
+ URLSearchParams: globalThis.URLSearchParams,
17207
17326
  wx,
17208
17327
  ...globals
17209
17328
  };
17210
17329
  }
17211
17330
  function createModuleLoader(registries, getCurrentPages, getApp, wxDriver, options) {
17212
17331
  const moduleCache = /* @__PURE__ */ new Map();
17213
- const executionContext = createExecutionContext(registries, getCurrentPages, getApp, wxDriver, options.kernel, options.globals ?? {});
17332
+ const executionContext = createExecutionContext(registries, getCurrentPages, getApp, wxDriver, options.kernel, options.globals ?? {}, options.onConsole);
17214
17333
  executionContext.globalThis = executionContext;
17215
17334
  function executeModule(filePath, loadContext) {
17216
17335
  const resolvedPath = path.resolve(filePath);
@@ -17240,9 +17359,13 @@ function createModuleLoader(registries, getCurrentPages, getApp, wxDriver, optio
17240
17359
  });
17241
17360
  localRequire.async = (request) => Promise.resolve().then(() => localRequire(request));
17242
17361
  try {
17243
- new vm.Script(`(function (exports, module, require, __filename, __dirname) { ${source}\n})`, { filename: resolvedPath }).runInNewContext(executionContext)(module.exports, module, localRequire, resolvedPath, path.dirname(resolvedPath));
17362
+ new vm.Script(`(function (exports, module, require, __filename, __dirname) { ${transformEsmToCommonJs(source)}\n})`, { filename: resolvedPath }).runInNewContext(executionContext)(module.exports, module, localRequire, resolvedPath, path.dirname(resolvedPath));
17244
17363
  const registeredDefinitions = [...registries.components.values()].filter((definition) => !registeredDefinitionsBefore.has(definition));
17245
- module.componentDefinitions = [.../* @__PURE__ */ new Set([...requiredComponentDefinitions, ...registeredDefinitions])];
17364
+ module.componentDefinitions = [.../* @__PURE__ */ new Set([
17365
+ ...requiredComponentDefinitions,
17366
+ ...loadContext?.componentDefinitions ?? [],
17367
+ ...registeredDefinitions
17368
+ ])];
17246
17369
  return module;
17247
17370
  } finally {
17248
17371
  registries.currentLoadContext = previousLoadContext;
@@ -17258,11 +17381,13 @@ function createModuleLoader(registries, getCurrentPages, getApp, wxDriver, optio
17258
17381
  return registries.appDefinition;
17259
17382
  },
17260
17383
  executePageModule(filePath, route) {
17261
- executeModule(filePath, {
17384
+ const fallbackDefinition = executeModule(filePath, {
17262
17385
  kind: "page",
17263
- route
17264
- });
17265
- const definition = registries.pages.get(route);
17386
+ route,
17387
+ componentDefinitions: []
17388
+ }).componentDefinitions.at(-1);
17389
+ const definition = registries.pages.get(route) ?? (fallbackDefinition ? normalizeComponentPageDefinition(fallbackDefinition) : void 0);
17390
+ if (!registries.pages.has(route) && definition) registerPageDefinition(registries, definition, route);
17266
17391
  if (!definition) throw new Error(`Page() was not registered for route "${route}" while executing ${normalize(filePath)}.`);
17267
17392
  return definition;
17268
17393
  },
@@ -19015,6 +19140,7 @@ var HeadlessSession = class {
19015
19140
  appDefinition = null;
19016
19141
  appInstance = null;
19017
19142
  moduleLoader;
19143
+ eventListeners = /* @__PURE__ */ new Map();
19018
19144
  registries;
19019
19145
  currentPageInstance = null;
19020
19146
  pages = [];
@@ -19156,7 +19282,8 @@ var HeadlessSession = class {
19156
19282
  }, {
19157
19283
  artifactSource: this.project.artifactSource,
19158
19284
  globals: options.globals,
19159
- kernel: this.kernel
19285
+ kernel: this.kernel,
19286
+ onConsole: (entry) => this.emit("console", entry)
19160
19287
  });
19161
19288
  }
19162
19289
  assertActive() {
@@ -19170,11 +19297,26 @@ var HeadlessSession = class {
19170
19297
  this.renderRequestPending = false;
19171
19298
  this.wxState.close();
19172
19299
  this.moduleLoader.close();
19300
+ this.eventListeners.clear();
19173
19301
  this.kernel.close();
19174
19302
  }
19175
19303
  getDiagnostics() {
19176
19304
  return this.kernel.diagnostics.getEntries();
19177
19305
  }
19306
+ on(eventName, handler) {
19307
+ if (!eventName || typeof handler !== "function") return;
19308
+ const listeners = this.eventListeners.get(eventName) ?? /* @__PURE__ */ new Set();
19309
+ listeners.add(handler);
19310
+ this.eventListeners.set(eventName, listeners);
19311
+ }
19312
+ removeListener(eventName, handler) {
19313
+ const listeners = this.eventListeners.get(eventName);
19314
+ listeners?.delete(handler);
19315
+ if (listeners?.size === 0) this.eventListeners.delete(eventName);
19316
+ }
19317
+ emit(eventName, ...args) {
19318
+ for (const handler of [...this.eventListeners.get(eventName) ?? []]) handler(...args);
19319
+ }
19178
19320
  get isClosed() {
19179
19321
  return this.kernel.isClosed;
19180
19322
  }
@@ -19186,6 +19328,10 @@ var HeadlessSession = class {
19186
19328
  this.assertActive();
19187
19329
  return this.pages.slice();
19188
19330
  }
19331
+ getWx() {
19332
+ this.assertActive();
19333
+ return this.moduleLoader.wx;
19334
+ }
19189
19335
  callWxMethod(methodName, ...args) {
19190
19336
  this.assertActive();
19191
19337
  const method = this.moduleLoader.wx[methodName];
@@ -19581,9 +19727,19 @@ var HeadlessSession = class {
19581
19727
  this.appDefinition = this.moduleLoader.executeAppModule(appModulePath);
19582
19728
  this.appInstance = createAppInstance(this.appDefinition);
19583
19729
  this.appInstance.onLaunch?.(launchOptions);
19730
+ this.syncRuntimeAppState();
19584
19731
  this.appInstance.onShow?.(launchOptions);
19585
19732
  return this.appInstance;
19586
19733
  }
19734
+ syncRuntimeAppState() {
19735
+ const runtimeApp = this.appInstance?.__wevuRuntimeApp;
19736
+ if (!runtimeApp || typeof runtimeApp !== "object" || !this.appInstance) return;
19737
+ if (runtimeApp.globalData && typeof runtimeApp.globalData === "object") this.appInstance.globalData = {
19738
+ ...this.appInstance.globalData,
19739
+ ...runtimeApp.globalData
19740
+ };
19741
+ if (Object.hasOwn(runtimeApp, "routes")) this.appInstance.routes = runtimeApp.routes;
19742
+ }
19587
19743
  reLaunch(url) {
19588
19744
  const target = this.resolveNavigationTarget(url);
19589
19745
  this.bootstrap(createAppLaunchOptions(target.normalizedRoute, target.query));
@@ -20231,7 +20387,10 @@ var HeadlessTestingPageHandle = class {
20231
20387
  return await runWithTimeout(async () => {
20232
20388
  this.session?.renderCurrentPage();
20233
20389
  const method = this.page[normalizedMethodName];
20234
- if (typeof method !== "function") throw new TypeError(`Method "${normalizedMethodName}" does not exist on headless page ${this.page.route}.`);
20390
+ if (typeof method !== "function") {
20391
+ if (options.routeOnly) return;
20392
+ throw new TypeError(`Method "${normalizedMethodName}" does not exist on headless page ${this.page.route}.`);
20393
+ }
20235
20394
  return cloneProtocolValue(await method.apply(this.page, args));
20236
20395
  }, options.timeout, `Timed out calling page method "${normalizedMethodName}" in headless testing runtime.`);
20237
20396
  }
@@ -20406,6 +20565,24 @@ var HeadlessTestingSessionHandle = class {
20406
20565
  async close() {
20407
20566
  this.session.close();
20408
20567
  }
20568
+ on(eventName, handler) {
20569
+ this.session.on(eventName, handler);
20570
+ return this;
20571
+ }
20572
+ removeListener(eventName, handler) {
20573
+ this.session.removeListener(eventName, handler);
20574
+ return this;
20575
+ }
20576
+ off(eventName, handler) {
20577
+ return this.removeListener(eventName, handler);
20578
+ }
20579
+ async evaluate(evaluator, ...args) {
20580
+ return await this.evaluateWithOptions(evaluator, {}, ...args);
20581
+ }
20582
+ async evaluateWithOptions(evaluator, options = {}, ...args) {
20583
+ this.session.assertActive();
20584
+ return await runWithTimeout(() => this.evaluateInRuntime(evaluator, args), options.timeout, "Timed out evaluating code in headless testing runtime.");
20585
+ }
20409
20586
  async currentPage() {
20410
20587
  this.session.assertActive();
20411
20588
  const current = this.session.getCurrentPages().at(-1);
@@ -20502,12 +20679,45 @@ var HeadlessTestingSessionHandle = class {
20502
20679
  if (!page) throw new Error(`Failed to ${methodName} route "${route}" through the headless wx runtime.`);
20503
20680
  return page;
20504
20681
  }
20682
+ async evaluateInRuntime(evaluator, args) {
20683
+ const runtimeGlobals = {
20684
+ getApp: () => this.session.getApp(),
20685
+ getCurrentPages: () => this.session.getCurrentPages(),
20686
+ wx: this.session.getWx()
20687
+ };
20688
+ const host = globalThis;
20689
+ const previous = /* @__PURE__ */ new Map();
20690
+ for (const [name, value] of Object.entries(runtimeGlobals)) {
20691
+ previous.set(name, Object.getOwnPropertyDescriptor(host, name));
20692
+ Object.defineProperty(host, name, {
20693
+ configurable: true,
20694
+ enumerable: false,
20695
+ value,
20696
+ writable: true
20697
+ });
20698
+ }
20699
+ try {
20700
+ const fn = typeof evaluator === "string" ? new vm.Script(`(${evaluator})`).runInThisContext() : evaluator;
20701
+ if (typeof fn !== "function") throw new TypeError("Headless evaluator must be a function or function source string.");
20702
+ return await fn(...args);
20703
+ } finally {
20704
+ for (const [name, descriptor] of previous) if (descriptor) Object.defineProperty(host, name, descriptor);
20705
+ else delete host[name];
20706
+ }
20707
+ }
20505
20708
  };
20506
20709
  //#endregion
20507
20710
  //#region ../../mpcore/packages/simulator/src/testing/launch.ts
20711
+ function resolveInitialRoute(session) {
20712
+ const entryPagePath = typeof session.project.appConfig.entryPagePath === "string" ? session.project.appConfig.entryPagePath.trim().replace(/^\/+/, "") : "";
20713
+ if (entryPagePath && session.project.routes.some((route) => route.route === entryPagePath)) return entryPagePath;
20714
+ return session.project.routes[0]?.route ?? null;
20715
+ }
20508
20716
  async function launch(options) {
20509
20717
  const session = createHeadlessSession({ projectPath: options.projectPath });
20510
20718
  session.bootstrap();
20719
+ const initialRoute = resolveInitialRoute(session);
20720
+ if (initialRoute) session.reLaunch(`/${initialRoute}`);
20511
20721
  return new HeadlessTestingSessionHandle(session.project, session);
20512
20722
  }
20513
20723
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@weapp-vite/miniprogram-automator",
3
3
  "type": "module",
4
- "version": "1.2.13",
4
+ "version": "1.2.14",
5
5
  "description": "完全兼容微信 miniprogram-automator 的现代化替代实现",
6
6
  "author": "ice breaker <1324318532@qq.com>",
7
7
  "license": "MIT",