@mantou/gem 2.2.13 → 2.2.15

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.
Files changed (73) hide show
  1. package/dist/gem.js +7 -7
  2. package/dist/gem.mjs +7 -7
  3. package/elements/base/gesture.d.ts +7 -1
  4. package/elements/base/gesture.d.ts.map +1 -1
  5. package/elements/base/gesture.js +23 -15
  6. package/elements/base/gesture.js.map +1 -1
  7. package/elements/base/link.d.ts.map +1 -1
  8. package/elements/base/link.js +6 -3
  9. package/elements/base/link.js.map +1 -1
  10. package/elements/base/modal-factory.d.ts +1 -1
  11. package/elements/base/modal-factory.d.ts.map +1 -1
  12. package/elements/base/use.d.ts.map +1 -1
  13. package/elements/base/use.js +11 -0
  14. package/elements/base/use.js.map +1 -1
  15. package/helper/hmr.d.ts +3 -2
  16. package/helper/hmr.d.ts.map +1 -1
  17. package/helper/hmr.js +95 -23
  18. package/helper/hmr.js.map +1 -1
  19. package/helper/i18n.d.ts +2 -1
  20. package/helper/i18n.d.ts.map +1 -1
  21. package/helper/i18n.js +9 -1
  22. package/helper/i18n.js.map +1 -1
  23. package/helper/theme.d.ts.map +1 -1
  24. package/helper/theme.js +5 -0
  25. package/helper/theme.js.map +1 -1
  26. package/lib/decorators.d.ts +9 -1
  27. package/lib/decorators.d.ts.map +1 -1
  28. package/lib/decorators.js +4 -3
  29. package/lib/decorators.js.map +1 -1
  30. package/lib/history.d.ts +10 -7
  31. package/lib/history.d.ts.map +1 -1
  32. package/lib/history.js +20 -16
  33. package/lib/history.js.map +1 -1
  34. package/lib/lit-html.d.ts +15 -0
  35. package/lib/lit-html.d.ts.map +1 -1
  36. package/lib/lit-html.js +3 -1
  37. package/lib/lit-html.js.map +1 -1
  38. package/lib/reactive.d.ts +4 -1
  39. package/lib/reactive.d.ts.map +1 -1
  40. package/lib/reactive.js +41 -18
  41. package/lib/reactive.js.map +1 -1
  42. package/lib/server.d.ts +14 -0
  43. package/lib/server.d.ts.map +1 -0
  44. package/lib/server.js +43 -0
  45. package/lib/server.js.map +1 -0
  46. package/lib/store.d.ts +10 -1
  47. package/lib/store.d.ts.map +1 -1
  48. package/lib/store.js +36 -9
  49. package/lib/store.js.map +1 -1
  50. package/lib/utils.d.ts +2 -2
  51. package/lib/utils.d.ts.map +1 -1
  52. package/lib/utils.js +20 -7
  53. package/lib/utils.js.map +1 -1
  54. package/lib/version.d.ts +1 -1
  55. package/lib/version.js +1 -1
  56. package/package.json +1 -1
  57. package/src/elements/base/gesture.ts +26 -15
  58. package/src/elements/base/link.ts +5 -3
  59. package/src/elements/base/use.ts +11 -0
  60. package/src/helper/hmr.ts +105 -37
  61. package/src/helper/i18n.ts +12 -1
  62. package/src/helper/theme.ts +4 -0
  63. package/src/lib/decorators.ts +10 -10
  64. package/src/lib/history.ts +20 -16
  65. package/src/lib/lit-html.ts +3 -1
  66. package/src/lib/reactive.ts +40 -18
  67. package/src/lib/server.ts +45 -0
  68. package/src/lib/store.ts +40 -9
  69. package/src/lib/utils.ts +22 -7
  70. package/src/lib/version.ts +1 -1
  71. package/src/test/elements/gesture.test.ts +26 -1
  72. package/src/test/gem-element/basic.test.ts +20 -0
  73. package/src/test/store.test.ts +12 -0
@@ -1,11 +1,13 @@
1
1
  // biome-ignore-all lint/plugin/no_this_effect: 兼容生命周期
2
2
 
3
3
  import type * as Lit from './lit-html';
4
+ import { isServer } from './server';
4
5
  import type { Store } from './store';
5
6
  import { connect } from './store';
6
7
  import {
7
8
  addListener,
8
9
  addMicrotask,
10
+ assignState,
9
11
  createUpdater,
10
12
  GemError,
11
13
  isArrayChange,
@@ -29,10 +31,18 @@ let hydrationEngine = (_value: unknown, _container: HTMLElement | DocumentFragme
29
31
  return;
30
32
  };
31
33
 
32
- export function setHydrationEngine(engine: typeof hydrationEngine) {
34
+ // 在元素首次渲染前恢复 SSR 的状态
35
+ let restoreState: ((ele: GemElement) => void) | undefined;
36
+
37
+ export function setHydrationEngine(engine: typeof hydrationEngine, restore?: typeof restoreState) {
33
38
  hydrationEngine = engine;
39
+ restoreState = restore;
34
40
  }
35
41
 
42
+ /** 渲染元素内容,SSR 时由水合引擎接管 */
43
+ export const _renderRoot = (value: unknown, container: HTMLElement | DocumentFragment) =>
44
+ hydrationEngine(value, container) || render(value, container);
45
+
36
46
  const { assign, defineProperty } = Object;
37
47
 
38
48
  // https://github.com/tc39/proposal-decorator-metadata
@@ -40,8 +50,6 @@ defineProperty(Symbol, 'metadata', { value: Symbol.for('Symbol.metadata') });
40
50
 
41
51
  const execCallback = (fun: any) => typeof fun === 'function' && fun();
42
52
 
43
- const ssrNativeQueueMicrotask = (globalThis as any).nQM;
44
-
45
53
  // 读取构造样式表,跨多个 gem 工作
46
54
  export const SheetToken = Symbol.for('gem@sheetToken');
47
55
  export type SheetTokenType = typeof SheetToken;
@@ -186,7 +194,7 @@ const updateStyleSheets = (map: Map<CSSStyleSheet, number>, sheets: CSSStyleShee
186
194
  if (needUpdate) {
187
195
  // 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
188
196
  // 在 SSR 中应该区别 effect 强制执行
189
- addMicrotask(rootUpdateFnMap.get(map)!, ssrNativeQueueMicrotask);
197
+ addMicrotask(rootUpdateFnMap.get(map)!);
190
198
  }
191
199
  };
192
200
 
@@ -286,10 +294,14 @@ const noBlockingTaskList = new LinkedList<() => void>();
286
294
  const tick = (timeStamp = performance.now()) => {
287
295
  if (performance.now() > timeStamp + 50) return requestAnimationFrame(tick);
288
296
  const task = noBlockingTaskList.get();
289
- if (task) {
297
+ if (!task) return;
298
+ // 出错时继续执行后面的任务,否则剩下的任务不会再被调度
299
+ try {
290
300
  task();
291
- tick(timeStamp);
301
+ } catch (err) {
302
+ reportError(err);
292
303
  }
304
+ tick(timeStamp);
293
305
  };
294
306
  noBlockingTaskList.addEventListener('start', () => addMicrotask(tick));
295
307
 
@@ -340,14 +352,14 @@ export abstract class GemElement extends HTMLElement {
340
352
  const effect = ele.#effectList.at(0);
341
353
  // https://github.com/mantou132/gem/issues/203
342
354
  // SSR 不会执行 initEffect,应该跳过
343
- if (!ssrNativeQueueMicrotask && ele.#isMounted && effect && !effect.initialized) {
355
+ if (!isServer && ele.#isMounted && effect && !effect.initialized) {
344
356
  throw new GemError(`Don't set state sync before insert the DOM`);
345
357
  }
346
- assign(state, payload);
358
+ assignState(state, payload);
347
359
  // 避免无限刷新
348
360
  // 挂载前 set state 不应该触发更新
349
361
  // SSR 中渲染 <gem-route>,其他情况的 SSR 应该也可以执行
350
- if (!ele.#rendering && ele.#isMounted) addMicrotask(ele.#update, ssrNativeQueueMicrotask);
362
+ if (!ele.#rendering && ele.#isMounted) addMicrotask(ele.#update);
351
363
  });
352
364
  ele.#internals.stateList.push(state);
353
365
  return state;
@@ -429,11 +441,17 @@ export abstract class GemElement extends HTMLElement {
429
441
  const isLight = this.#renderRoot === this;
430
442
  const temp = item ? item.render() : isLight ? undefined : html`<slot></slot>`;
431
443
  if (temp === undefined) return;
432
- const r = temp !== null ? temp : html`<style>${isLight ? '@scope' : ':host'}{&{display:none!important;}}</style>`;
433
- hydrationEngine(r, this.#renderRoot) || render(r, this.#renderRoot);
444
+ // `<style>` 中不使用绑定,SSR 水合不支持
445
+ const r =
446
+ temp ??
447
+ (isLight
448
+ ? html`<style>@scope{&{display:none!important;}}</style>`
449
+ : html`<style>:host{&{display:none!important;}}</style>`);
450
+ _renderRoot(r, this.#renderRoot);
434
451
  } catch (err) {
435
- this.dispatchEvent(new CustomEvent(_RenderErrorEvent, { bubbles: true, composed: true, detail: err }));
436
- throw err;
452
+ const evt = new CustomEvent(_RenderErrorEvent, { bubbles: true, composed: true, cancelable: true, detail: err });
453
+ // `@fallback` 处理了错误
454
+ if (this.dispatchEvent(evt)) throw err;
437
455
  } finally {
438
456
  this.#rendering = false;
439
457
  }
@@ -447,12 +465,13 @@ export abstract class GemElement extends HTMLElement {
447
465
  (item || !this.#renderList.length)
448
466
  ) {
449
467
  this.#render(item);
450
- addMicrotask(this.#execEffect);
468
+ if (!isServer) addMicrotask(this.#execEffect);
451
469
  }
452
470
  };
453
471
 
454
472
  #update = () => {
455
- if (this.#metadata.noBlocking) {
473
+ // SSR 需要同步渲染
474
+ if (this.#metadata.noBlocking && !isServer) {
456
475
  noBlockingTaskList.add(this.#updateCallback);
457
476
  } else {
458
477
  this.#updateCallback();
@@ -506,7 +525,8 @@ export abstract class GemElement extends HTMLElement {
506
525
  };
507
526
 
508
527
  #disconnectStore?: (() => void)[];
509
- #connectedCallback = async () => {
528
+ // 同步执行,SSR 需要捕获渲染错误
529
+ #connectedCallback = () => {
510
530
  if (this.#isAppendReason) {
511
531
  this.#isAppendReason = false;
512
532
  // https://github.com/mantou132/gem/issues/202
@@ -526,12 +546,14 @@ export abstract class GemElement extends HTMLElement {
526
546
  if (!mode && this.#renderList.length && !penetrable) this.#internals.states.add(BoundaryCSSState);
527
547
 
528
548
  this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update));
549
+ restoreState?.(this);
529
550
  this.#render(this.#renderItem);
530
551
  this.#isMounted = true;
531
552
  this.#clearStyle = this.#prepareStyle();
532
553
  // 等待所有元素的样式被应用,再执行回调
533
554
  // 这让 mounted 和 effect 回调和其他回调一样保持一样的异步行为
534
- addMicrotask(this.#initEffect);
555
+ // SSR 不执行副作用
556
+ if (!isServer) addMicrotask(this.#initEffect);
535
557
  };
536
558
 
537
559
  /**
@@ -540,7 +562,7 @@ export abstract class GemElement extends HTMLElement {
540
562
  * use `@mounted`
541
563
  */
542
564
  connectedCallback(): Unique {
543
- if (this.#metadata.noBlocking) {
565
+ if (this.#metadata.noBlocking && !isServer) {
544
566
  noBlockingTaskList.add(this.#connectedCallback);
545
567
  } else {
546
568
  this.#connectedCallback();
@@ -0,0 +1,45 @@
1
+ // SSR 相关的功能都在这里
2
+ //
3
+ // `@mantou/gem-ssr` 在加载 Gem 前提供 `__GEM_SSR__`,返回当前请求的作用域,
4
+ // 客户端构建时 `unplugin-gem` 把 `globalThis.__GEM_SSR__` 替换成 `undefined`,SSR 代码会被压缩移除
5
+
6
+ const { get, set, has, deleteProperty, defineProperty, getOwnPropertyDescriptor, ownKeys } = Reflect;
7
+
8
+ const getRequestScope: (() => WeakMap<object, any> | undefined) | undefined = (globalThis as any).__GEM_SSR__;
9
+
10
+ /** 服务端渲染时不执行副作用,同步更新 */
11
+ export const isServer = !!getRequestScope;
12
+
13
+ /**
14
+ * SSR 时返回当前请求中 `key` 对应的值,第一次访问时使用 `init` 创建,
15
+ * 不在请求中(包括客户端)时返回 `key` 本身
16
+ */
17
+ export const getRequestValue: <T extends object>(key: T, init: () => T) => T = getRequestScope
18
+ ? (key, init) => {
19
+ const scope = getRequestScope();
20
+ if (!scope) return key;
21
+ if (!scope.has(key)) scope.set(key, init());
22
+ return scope.get(key);
23
+ }
24
+ : (key) => key;
25
+
26
+ /**
27
+ * SSR 时对象的读写转到当前请求的副本上,副本在第一次访问时从原对象浅复制
28
+ *
29
+ * 嵌套的对象没有复制,原地修改会影响所有请求,应该替换顶层属性,例如 `store({ list: [...store.list, item] })`
30
+ */
31
+ export const isolate: <T extends object>(target: T) => T = getRequestScope
32
+ ? (target) => {
33
+ const copy = () => ({ ...target });
34
+ const current = () => getRequestValue(target, copy);
35
+ return new Proxy(target, {
36
+ get: (_, key) => get(current(), key),
37
+ set: (_, key, value) => set(current(), key, value),
38
+ has: (_, key) => has(current(), key),
39
+ deleteProperty: (_, key) => deleteProperty(current(), key),
40
+ defineProperty: (_, key, descriptor) => defineProperty(current(), key, descriptor),
41
+ getOwnPropertyDescriptor: (_, key) => getOwnPropertyDescriptor(current(), key),
42
+ ownKeys: () => ownKeys(current()),
43
+ });
44
+ }
45
+ : (target) => target;
package/src/lib/store.ts CHANGED
@@ -1,22 +1,53 @@
1
- import { addMicrotask, createUpdater, type Updater } from './utils';
1
+ import { getRequestValue, isolate, isServer } from './server';
2
+ import { addMicrotask, assignState, createUpdater, type Updater } from './utils';
2
3
 
3
4
  export type Store<T = any> = Updater<T>;
4
5
 
5
6
  /**@internal */
6
7
  export const _StoreListenerMap = new WeakMap<Store<any>, Set<() => void>>();
7
8
 
8
- export function createStore<T = any>(originStore: T) {
9
- const store = createUpdater(originStore, (value?: Partial<T>) => {
10
- // biome-ignore lint/plugin/assign: 未知类型
11
- Object.assign(store, value);
12
- _StoreListenerMap.get(store)?.forEach((func) => addMicrotask(func));
13
- });
14
- _StoreListenerMap.set(store, new Set<() => void>());
9
+ const createListeners = () => new Set<() => void>();
10
+
11
+ // SSR 时当前请求中更新过的有 key 的 store,渲染后输出到页面中
12
+ const updatedStores = new Map<string, Store<any>>();
13
+
14
+ /** SSR 时当前请求中更新过的有 key 的 store,供 `@mantou/gem-ssr` 使用 */
15
+ export const _getUpdatedStores = () => getRequestValue(updatedStores, () => new Map());
16
+
17
+ // 客户端读取服务端渲染时 store 的值
18
+ let restoreStore: ((key: string) => object | undefined) | undefined;
19
+
20
+ /** 设置客户端恢复 store 的方法,供 `@mantou/gem-ssr` 使用 */
21
+ export function _setStoreRestorer(restorer: typeof restoreStore) {
22
+ restoreStore = restorer;
23
+ }
24
+
25
+ /**
26
+ * 创建 store,`key` 用来在 SSR 时同步服务端的值,`swc-plugin-gem` 的 `ssr` 选项会为模块顶层的 store 自动添加
27
+ */
28
+ export function createStore<T = any>(originStore: T, key?: string) {
29
+ const listeners = createListeners();
30
+ // SSR 时每个请求使用独立的值
31
+ const store = isolate(
32
+ createUpdater(originStore, (value?: Partial<T>) => {
33
+ assignState(store, value);
34
+ listeners.forEach(addMicrotask);
35
+ if (!isServer) return;
36
+ // SSR 时请求中添加的监听器只属于当前请求,其他请求更新 store 时不会触发
37
+ getRequestValue(listeners, createListeners).forEach(addMicrotask);
38
+ const updated = _getUpdatedStores();
39
+ // 不在请求中时更新的是全局的值,客户端也一样
40
+ if (key && updated !== updatedStores) updated.set(key, store);
41
+ }),
42
+ );
43
+ if (key && restoreStore) assignState(store, restoreStore(key));
44
+ _StoreListenerMap.set(store, listeners);
15
45
  return store;
16
46
  }
17
47
 
18
48
  export function connect(store: Store<any>, func: () => void) {
19
- const listeners = _StoreListenerMap.get(store);
49
+ const storeListeners = _StoreListenerMap.get(store);
50
+ const listeners = storeListeners && getRequestValue(storeListeners, createListeners);
20
51
  listeners?.add(func);
21
52
  return () => listeners?.delete(func);
22
53
  }
package/src/lib/utils.ts CHANGED
@@ -1,4 +1,6 @@
1
- const { assign, fromEntries, entries, keys } = Object;
1
+ import { getRequestValue } from './server';
2
+
3
+ const { assign, fromEntries, entries, keys, defineProperty, hasOwn } = Object;
2
4
 
3
5
  // 用原生 Chrome 找不到调用栈
4
6
  function queueMicrotask(cb: () => void) {
@@ -6,13 +8,16 @@ function queueMicrotask(cb: () => void) {
6
8
  }
7
9
 
8
10
  const microtaskSet = new Set<() => void>();
9
- export function addMicrotask(func: () => void, method = queueMicrotask) {
10
- if (microtaskSet.has(func)) return;
11
- method(() => {
12
- microtaskSet.delete(func);
11
+ const createMicrotaskSet = () => new Set<() => void>();
12
+ export function addMicrotask(func: () => void) {
13
+ // SSR 时不同请求的同一个函数都需要执行
14
+ const taskSet = getRequestValue(microtaskSet, createMicrotaskSet);
15
+ if (taskSet.has(func)) return;
16
+ queueMicrotask(() => {
17
+ taskSet.delete(func);
13
18
  func();
14
19
  });
15
- microtaskSet.add(func);
20
+ taskSet.add(func);
16
21
  }
17
22
 
18
23
  type OverrideFn = {
@@ -29,6 +34,16 @@ type OverrideFn = {
29
34
  [Symbol.metadata]: never;
30
35
  };
31
36
 
37
+ // 删除后继承的 `name` `length` 只读,直接赋值会报错,需要先定义成自有属性
38
+ export function assignState(state: any, payload: any) {
39
+ for (const key of ['name', 'length']) {
40
+ if (payload && hasOwn(payload, key)) {
41
+ defineProperty(state, key, { value: payload[key], writable: true, enumerable: true, configurable: true });
42
+ }
43
+ }
44
+ assign(state, payload);
45
+ }
46
+
32
47
  // 注意 typeof state === 'function' 但是没有 Function 的方法和属性
33
48
  export function createUpdater<T, Fn = (payload?: Partial<T>) => any>(initState: T, fn: Fn) {
34
49
  const state: any = fn;
@@ -36,7 +51,7 @@ export function createUpdater<T, Fn = (payload?: Partial<T>) => any>(initState:
36
51
  // setPrototypeOf(state, null);
37
52
  delete state.name;
38
53
  delete state.length;
39
- assign(state, initState);
54
+ assignState(state, initState);
40
55
  // ts 不允许直接 Omit 掉 Fn 上的属性
41
56
  // 只能利用 ts plugin 移除多余的自动完成项
42
57
  return state as Fn & Omit<OverrideFn, keyof T> & T;
@@ -1 +1 @@
1
- export const version = '2.2.13';
1
+ export const version = '2.2.15';
@@ -1,4 +1,4 @@
1
- import type { EndEventDetail, GemGestureElement, SwipeEventDetail } from '../../elements/gesture';
1
+ import type { EndEventDetail, GemGestureElement, PanEventDetail, SwipeEventDetail } from '../../elements/gesture';
2
2
  import { html } from '../../lib/element';
3
3
  import { aTimeout, expect, fixture } from '../utils';
4
4
 
@@ -240,6 +240,31 @@ describe('GemGestureElement swipe', () => {
240
240
  });
241
241
  }
242
242
 
243
+ it('locks the axis for the rest of a touch with touch-action=pan-y', async () => {
244
+ const { el, swipes, dispatch } = await setupGesture('pan-y');
245
+ const pans: number[] = [];
246
+ el.addEventListener('pan', (evt: CustomEvent<PanEventDetail>) => pans.push(evt.detail.x));
247
+ dispatch('pointerdown', [0, 0]);
248
+ dispatch('pointermove', [10, 0, 20]);
249
+ dispatch('pointermove', [20, 60, 20]);
250
+ dispatch('pointerup', [30, 120, 20]);
251
+ await Promise.resolve();
252
+ expect(pans).to.deep.equal([0, 0]);
253
+ expect(swipes).to.deep.equal([]);
254
+ });
255
+
256
+ it('emits raw horizontal movement once locked horizontally with touch-action=pan-y', async () => {
257
+ const { el, dispatch } = await setupGesture('pan-y');
258
+ const pans: number[] = [];
259
+ el.addEventListener('pan', (evt: CustomEvent<PanEventDetail>) => pans.push(evt.detail.x));
260
+ dispatch('pointerdown', [0, 0]);
261
+ dispatch('pointermove', [10, 5, 0]);
262
+ dispatch('pointermove', [20, 15, 0]);
263
+ dispatch('pointermove', [30, 20, 10]);
264
+ await Promise.resolve();
265
+ expect(pans).to.deep.equal([0, 10, 5]);
266
+ });
267
+
243
268
  it('allows horizontal flicks with touch-action=pan-y', async () => {
244
269
  const { swipes, dispatch } = await setupGesture('pan-y');
245
270
  dispatch('pointerdown', [0, 0]);
@@ -161,3 +161,23 @@ describe('基本 gem element 测试', () => {
161
161
  expect(el.renderCount).to.equal(2);
162
162
  });
163
163
  });
164
+
165
+ @customElement('gem-name-state')
166
+ class GemNameStateElement extends GemElement {
167
+ state = createState({ name: 'guest' });
168
+
169
+ render() {
170
+ return html`${this.state.name}`;
171
+ }
172
+ }
173
+
174
+ describe('state `name` 字段', () => {
175
+ it('create and update state', async () => {
176
+ const el: GemNameStateElement = await fixture(html`<gem-name-state></gem-name-state>`);
177
+ expect(el.textContent).to.equal('guest');
178
+ el.state({ name: 'admin' });
179
+ await Promise.resolve();
180
+ expect({ ...el.state }).to.deep.equal({ name: 'admin' });
181
+ expect(el.textContent).to.equal('admin');
182
+ });
183
+ });
@@ -25,3 +25,15 @@ describe('store 测试', () => {
25
25
  expect(flag).to.equal(true);
26
26
  });
27
27
  });
28
+
29
+ describe('store `name` `length` 字段', () => {
30
+ it('create and update store', () => {
31
+ const store = createStore({ name: 'guest', length: 1 });
32
+ expect({ ...store }).to.deep.equal({ name: 'guest', length: 1 });
33
+ store({ name: 'admin', length: 2 });
34
+ expect({ ...store }).to.deep.equal({ name: 'admin', length: 2 });
35
+ const store2 = createStore<{ name?: string }>({});
36
+ store2({ name: 'guest' });
37
+ expect(store2.name).to.equal('guest');
38
+ });
39
+ });