dsh-memento 0.5.12 → 0.5.13

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/client/client.js CHANGED
@@ -18,6 +18,12 @@
18
18
  if (typeof window === 'undefined' || !window.__ModuleLoader__ || !window.__ModuleLoader__.load) return
19
19
  window.__ModuleLoader__.load({
20
20
  id: 'dsh-memento',
21
+ /**
22
+ * 物化回调。require 是宿主模块系统(react 等平台内置模块)——本仓包里没有
23
+ * 这些模块的类型面,故按不透明函数面声明,返回值交给消费点自行收窄。
24
+ * @param {(id: string) => any} require - 宿主模块解析函数。
25
+ * @returns {{name: string, inject: string[], apply: (ctx: any) => void}} 客户端插件定义。
26
+ */
21
27
  factory: function (require) {
22
28
  const react = require('react')
23
29
  /** createElement 简写(宿主平台内置 react,无需构建期 JSX 编译)。 */
@@ -26,9 +32,14 @@
26
32
  const PANEL_ID = 'dsh-memento-panel'
27
33
 
28
34
  /** 本页已渲染的悬浮入口按钮引用(设置页 panel.enabled 开关即时切换用)。 */
35
+ /** @type {HTMLElement | null} */
29
36
  let panelOpenButton = null
30
37
 
31
- /** 悬浮窗开关的即时生效面:切显隐;开启时按钮缺失(上次探测为关)则重建。 */
38
+ /**
39
+ * 悬浮窗开关的即时生效面:切显隐;开启时按钮缺失(上次探测为关)则重建。
40
+ * @param {boolean} visible - 目标可见性。
41
+ * @returns {void}
42
+ */
32
43
  function setPanelButtonVisible(visible) {
33
44
  if (panelOpenButton === null) panelOpenButton = document.getElementById('mem-open')
34
45
  if (panelOpenButton !== null) {
@@ -38,7 +49,27 @@ function setPanelButtonVisible(visible) {
38
49
  }
39
50
  }
40
51
 
52
+ /**
53
+ * 面板文案模板。
54
+ * @typedef {object} PanelStrings
55
+ * @property {string} open
56
+ * @property {string} title
57
+ * @property {string} refresh
58
+ * @property {string} close
59
+ * @property {string} filter
60
+ * @property {string} empty
61
+ * @property {(shown: number, total: number) => string} truncated
62
+ * @property {(n: number) => string} groupCount
63
+ * @property {string} budgets
64
+ * @property {string} audit
65
+ * @property {string} loading
66
+ * @property {string} auditEmpty
67
+ * @property {string} proposals
68
+ * @property {string} proposalsEmpty
69
+ * @property {(message: string) => string} loadFailed
70
+ */
41
71
  /** 面板文案(en 源文 / zh 译文;语言来自 /api/memento/entries 响应的 language 字段,缺省 en)。 */
72
+ /** @type {Record<string, PanelStrings>} */
42
73
  const STRINGS = {
43
74
  en: {
44
75
  open: '🧠 Memory',
@@ -76,6 +107,46 @@ const STRINGS = {
76
107
  },
77
108
  }
78
109
 
110
+ /**
111
+ * 面板数据行。只声明面板真正消费的字段:服务端返回更宽的条目行,
112
+ * 面板只读展示 track/scope/text 并经预算表读 used/limit。
113
+ * @typedef {object} PanelEntryRow
114
+ * @property {string} id
115
+ * @property {string} track
116
+ * @property {string} scope
117
+ * @property {string} text
118
+ * @property {string} [workspaceKey]
119
+ * @property {string} [agentKey]
120
+ * @property {number} [createdAt]
121
+ * @property {number} [recallCount]
122
+ */
123
+ /**
124
+ * 预算用量行(/api/memento/entries 的 budgets[])。
125
+ * @typedef {object} PanelBudgetRow
126
+ * @property {string} track
127
+ * @property {string} scope
128
+ * @property {number} used
129
+ * @property {number} limit
130
+ */
131
+ /**
132
+ * 审计行(/api/memento/audit 的 rows[])。
133
+ * @typedef {object} PanelAuditRow
134
+ * @property {number} ts
135
+ * @property {string} action
136
+ * @property {string} [track]
137
+ * @property {string} [scope]
138
+ * @property {string} [outcome]
139
+ * @property {string} [source]
140
+ */
141
+ /**
142
+ * 待审批提案(/api/memento/proposals 的 proposals[])。
143
+ * @typedef {object} PanelProposalRow
144
+ * @property {string} id
145
+ * @property {string} track
146
+ * @property {string} scope
147
+ * @property {string} text
148
+ * @property {number} createdAt
149
+ */
79
150
  /**
80
151
  * 启动探测:panel.enabled=false 时入口按钮不渲染(设置面板可随时改回);
81
152
  * 探测失败按开启处理,行为与未引入开关前的版本一致。
@@ -102,6 +173,11 @@ async function bootPanel() {
102
173
  installPanel(state)
103
174
  }
104
175
 
176
+ /**
177
+ * 挂载面板(入口按钮 + 抽屉)。
178
+ * @param {{enabled: boolean, language: string}} state - 启动探测结果。
179
+ * @returns {void}
180
+ */
105
181
  function installPanel(state) {
106
182
  if (document.getElementById(PANEL_ID)) return
107
183
  let S = STRINGS[state.language] ?? STRINGS.en
@@ -155,13 +231,17 @@ function installPanel(state) {
155
231
  document.head.appendChild(style)
156
232
 
157
233
  const body = document.getElementById('mem-body')
158
- const filter = document.getElementById('mem-filter')
234
+ const filter = /** @type {HTMLInputElement} */ (document.getElementById('mem-filter'))
159
235
  const titleLabel = document.getElementById('mem-title')
160
236
  const refreshBtn = document.getElementById('mem-refresh')
161
237
  const closeBtn = document.getElementById('mem-close')
162
- const filterInput = document.getElementById('mem-filter')
238
+ const filterInput = /** @type {HTMLInputElement} */ (document.getElementById('mem-filter'))
163
239
 
164
- /** 按服务端 language 切换文案并刷新静态标签(语言随配置,运行期不变)。 */
240
+ /**
241
+ * 按服务端 language 切换文案并刷新静态标签(语言随配置,运行期不变)。
242
+ * @param {string} language - 服务端 language 字段。
243
+ * @returns {void}
244
+ */
165
245
  const applyLanguage = (language) => {
166
246
  S = STRINGS[language] ?? STRINGS.en
167
247
  openBtn.textContent = S.open
@@ -172,6 +252,7 @@ function installPanel(state) {
172
252
  filterInput.placeholder = S.filter
173
253
  }
174
254
 
255
+ /** @type {PanelEntryRow[]} */
175
256
  let entries = []
176
257
  let lastFilter = ''
177
258
  let lastTotal = 0
@@ -203,6 +284,11 @@ function installPanel(state) {
203
284
  body.innerHTML = html
204
285
  }
205
286
 
287
+ /**
288
+ * 渲染预算条与两个异步槽位骨架。
289
+ * @param {PanelBudgetRow[]} budgets - 预算用量行。
290
+ * @returns {void}
291
+ */
206
292
  const renderBudget = (budgets) => {
207
293
  if (!Array.isArray(budgets) || budgets.length === 0) return
208
294
  let html = `<div class="mem-group">${S.budgets}</div>`
@@ -217,6 +303,11 @@ function installPanel(state) {
217
303
  body.insertAdjacentHTML('beforeend', html)
218
304
  }
219
305
 
306
+ /**
307
+ * 渲染审计槽位。
308
+ * @param {PanelAuditRow[]} rows - 审计行。
309
+ * @returns {void}
310
+ */
220
311
  const renderAudit = (rows) => {
221
312
  const slot = document.getElementById('mem-audit-slot')
222
313
  if (slot === null) return
@@ -228,6 +319,11 @@ function installPanel(state) {
228
319
  `<div class="mem-audit">${new Date(row.ts).toLocaleString()} ${escapeHtml(row.action)}${row.track ? ` ${escapeHtml(row.track)}/${escapeHtml(row.scope)}` : ''} · ${escapeHtml(row.outcome ?? '')} · ${escapeHtml(row.source ?? '')}</div>`).join('')
229
320
  }
230
321
 
322
+ /**
323
+ * 渲染待审批提案槽位。
324
+ * @param {PanelProposalRow[]} proposals - 待审批提案。
325
+ * @returns {void}
326
+ */
231
327
  const renderProposals = (proposals) => {
232
328
  const slot = document.getElementById('mem-proposal-slot')
233
329
  if (slot === null) return
@@ -276,12 +372,18 @@ function installPanel(state) {
276
372
  })
277
373
  }
278
374
 
375
+ /**
376
+ * HTML 转义(面板所有插值文本都经此,避免记忆内容当标记注入)。
377
+ * @param {unknown} value - 待转义值(非字符串先 String())。
378
+ * @returns {string} 转义后的文本。
379
+ */
279
380
  function escapeHtml(value) {
280
381
  return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;')
281
382
  }
282
383
 
283
384
  // ── 宿主设置页(settings.section 一级项,id = dsh-memento)──────────────
284
385
  /** 卡片文案(en 源文 / zh 译文;语言跟随 namespace value.language,保存后即时切换)。 */
386
+ /** @type {Record<string, Record<string, string>>} */
285
387
  const CARD_STRINGS = {
286
388
  en: {
287
389
  title: 'dsh-memento memory',
@@ -392,9 +494,14 @@ function escapeHtml(value) {
392
494
  /** 顶层字段名(保存按顶层聚合:scope.set(topField, 合并值),不依赖点路径写入面)。 */
393
495
  const POLICIES = ['ask', 'auto', 'off']
394
496
 
395
- /** 简版快照 store(useSyncExternalStore 形状:{getSnapshot, subscribe}+set)。 */
497
+ /**
498
+ * 简版快照 store(useSyncExternalStore 形状:{getSnapshot, subscribe}+set)。
499
+ * @param {Record<string, any>} initial - 顶层设置值。
500
+ * @returns {{getSnapshot: () => Record<string, any>, subscribe: (fn: () => void) => () => void, set: (next: Record<string, any>) => void}} store。
501
+ */
396
502
  function createSnapshotStore(initial) {
397
503
  let snapshot = initial
504
+ /** @type {Set<() => void>} */
398
505
  const listeners = new Set()
399
506
  return {
400
507
  getSnapshot() { return snapshot },
@@ -403,7 +510,12 @@ function escapeHtml(value) {
403
510
  }
404
511
  }
405
512
 
406
- /** 读点路径('a.b' → obj?.a?.b)。 */
513
+ /**
514
+ * 读点路径('a.b' → obj?.a?.b)。
515
+ * @param {any} obj - 起点对象。
516
+ * @param {string} path - 点路径。
517
+ * @returns {any} 路径值(缺失为 undefined)。
518
+ */
407
519
  function pathValue(obj, path) {
408
520
  let cursor = obj
409
521
  for (const key of path.split('.')) {
@@ -413,7 +525,12 @@ function escapeHtml(value) {
413
525
  return cursor
414
526
  }
415
527
 
416
- /** 判定子字段是否被用户层覆盖(user 层子路径 hasOwn)。 */
528
+ /**
529
+ * 判定子字段是否被用户层覆盖(user 层子路径 hasOwn)。
530
+ * @param {any} user - 设置用户层。
531
+ * @param {string} path - 点路径。
532
+ * @returns {boolean} 用户层是否显式写了该子字段。
533
+ */
417
534
  function pathStored(user, path) {
418
535
  const keys = path.split('.')
419
536
  const last = keys.pop()
@@ -425,7 +542,12 @@ function escapeHtml(value) {
425
542
  return cursor !== null && typeof cursor === 'object' && Object.hasOwn(cursor, last)
426
543
  }
427
544
 
428
- /** 深合并(草稿子树 → 当前顶层值;数组与标量直接替换)。 */
545
+ /**
546
+ * 深合并(草稿子树 → 当前顶层值;数组与标量直接替换)。
547
+ * @param {any} base - 当前值。
548
+ * @param {any} patch - 草稿子树。
549
+ * @returns {any} 合并结果。
550
+ */
429
551
  function deepMerge(base, patch) {
430
552
  if (patch === null || typeof patch !== 'object' || Array.isArray(patch)) return patch
431
553
  const out = { ...(base !== null && typeof base === 'object' && !Array.isArray(base) ? base : {}) }
@@ -463,7 +585,13 @@ function escapeHtml(value) {
463
585
  const SPEC_BY_PATH = new Map(FIELD_SPECS.map((spec) => [spec.path, spec]))
464
586
  const RELOAD_PATHS = new Set(['snapshotOrder'])
465
587
 
466
- /** 草稿文本 → 顶层字段写入计划;无法解析的草稿返回 undefined(阻塞保存)。 */
588
+ /**
589
+ * 草稿文本 → 顶层字段写入计划;无法解析的草稿返回 undefined(阻塞保存)。
590
+ * @param {{path: string, kind: string, key: string, choices?: string[]}} spec - 子字段规格。
591
+ * @param {string} text - 控件里的草稿文本。
592
+ * @param {any} currentValue - 当前快照值(text 类字段用于回退判定)。
593
+ * @returns {{ok: true, value: any} | {ok: false}} 解析结果。
594
+ */
467
595
  function parseDraftText(spec, text, currentValue) {
468
596
  if (spec.kind === 'bool') {
469
597
  if (text === 'true') return { ok: true, value: true }
@@ -479,6 +607,7 @@ function escapeHtml(value) {
479
607
  return trimmed !== '' && Number.isFinite(parsed) && Number.isInteger(parsed) ? { ok: true, value: parsed } : { ok: false }
480
608
  }
481
609
  if (spec.kind === 'policies') {
610
+ /** @type {Record<string, string>} */
482
611
  const next = {}
483
612
  for (const rawLine of text.split('\n')) {
484
613
  const line = rawLine.trim()
@@ -497,14 +626,22 @@ function escapeHtml(value) {
497
626
  return { ok: true, value: text }
498
627
  }
499
628
 
500
- /** 子字段格式化(快照值 → 控件文本)。 */
629
+ /**
630
+ * 子字段格式化(快照值 → 控件文本)。
631
+ * @param {any} value - 快照值。
632
+ * @returns {string} 控件文本。
633
+ */
501
634
  function formatValue(value) {
502
635
  if (value === undefined || value === null) return ''
503
636
  if (typeof value === 'boolean') return value ? 'true' : 'false'
504
637
  return String(value)
505
638
  }
506
639
 
507
- /** policies 顶层对象的控件文本(每行 key=policy)。 */
640
+ /**
641
+ * policies 顶层对象的控件文本(每行 key=policy)。
642
+ * @param {any} value - policies 顶层对象。
643
+ * @returns {string} 多行文本。
644
+ */
508
645
  function formatPolicies(value) {
509
646
  if (value === null || typeof value !== 'object') return ''
510
647
  return Object.entries(value).map(([key, policy]) => `${key}=${policy}`).join('\n')
@@ -515,6 +652,10 @@ function escapeHtml(value) {
515
652
  * 顶层聚合:同顶层字段的多个子字段草稿一次 scope.set(top, 合并值)。
516
653
  */
517
654
  class CardForm {
655
+ /**
656
+ * @param {any} scope - 宿主 settingsScope 句柄。
657
+ * @param {(tops: Map<string, object>) => void} onLanded - 落盘成功回调(panel 显隐同步用)。
658
+ */
518
659
  constructor(scope, onLanded) {
519
660
  this.scope = scope
520
661
  /** @type {((tops: Map<string, object>) => void) | undefined} 落盘成功回调(panel 显隐同步用)。 */
@@ -527,12 +668,23 @@ function escapeHtml(value) {
527
668
  scope.subscribe(() => this.publish())
528
669
  }
529
670
 
671
+ /**
672
+ * 绑定读取投影。
673
+ * @param {() => Record<string, any>} project - 快照投影(每次读当前 scope)。
674
+ * @returns {{getSnapshot: () => Record<string, any>, subscribe: (fn: () => void) => () => void, set: (next: Record<string, any>) => void}} store。
675
+ */
530
676
  bind(project) {
531
677
  const store = createSnapshotStore(project())
532
678
  this.listeners.add(() => store.set(project()))
533
679
  return store
534
680
  }
535
681
 
682
+ /**
683
+ * 暂存一个子字段草稿。
684
+ * @param {string} path - 子字段点路径。
685
+ * @param {string} text - 控件文本。
686
+ * @returns {void}
687
+ */
536
688
  stage(path, text) {
537
689
  this.staged.set(path, text)
538
690
  this.failed = false
@@ -562,7 +714,18 @@ function escapeHtml(value) {
562
714
  const base = tops.get(top) ?? (snapshot.value !== null && typeof snapshot.value === 'object' ? snapshot.value[top] : undefined)
563
715
  const merged = keys.length === 1
564
716
  ? parsed.value
565
- : deepMerge(base, (() => { /** @type {Record<string, unknown>} */ const out = {}; let cursor = out; for (let i = 1; i < keys.length - 1; i++) { cursor[keys[i]] = {}; cursor = cursor[keys[i]] } cursor[keys[keys.length - 1]] = parsed.value; return out })())
717
+ : deepMerge(base, (() => {
718
+ /** @type {Record<string, any>} */
719
+ const out = {}
720
+ /** @type {Record<string, any>} */
721
+ let cursor = out
722
+ for (let i = 1; i < keys.length - 1; i++) {
723
+ cursor[keys[i]] = {}
724
+ cursor = cursor[keys[i]]
725
+ }
726
+ cursor[keys[keys.length - 1]] = parsed.value
727
+ return out
728
+ })())
566
729
  tops.set(top, merged)
567
730
  }
568
731
  if (!valid || this.staged.size === 0) { this.publish(); return }
@@ -584,6 +747,11 @@ function escapeHtml(value) {
584
747
  this.publish()
585
748
  }
586
749
 
750
+ /**
751
+ * 单个子字段的控件状态(草稿或快照文本 / 是否被用户层覆盖 / 草稿是否非法)。
752
+ * @param {string} path - 子字段点路径。
753
+ * @returns {{text: string, overridden: boolean, invalid: boolean}} 控件状态。
754
+ */
587
755
  fieldState(path) {
588
756
  const spec = SPEC_BY_PATH.get(path)
589
757
  const snapshot = this.scope.getSnapshot()
@@ -664,7 +832,11 @@ function escapeHtml(value) {
664
832
  .memsec-btn:disabled { opacity: 0.5; cursor: default; }
665
833
  `
666
834
 
667
- /** 单字段控件行(label + input/checkbox/select + override 徽标 + reset + hint/invalid)。 */
835
+ /**
836
+ * 单字段控件行(label + input/checkbox/select + override 徽标 + reset + hint/invalid)。
837
+ * @param {{t: (key: string) => string, spec: {path: string, kind: string, key: string, choices?: string[]}, state: {text: string, invalid: boolean, overridden: boolean}, disabled: boolean, onEdit: (text: string) => void, onReset: () => void}} props - 组件属性。
838
+ * @returns {unknown} react 元素。
839
+ */
668
840
  function FieldRow(props) {
669
841
  const { t, spec, state, disabled, onEdit, onReset } = props
670
842
  const label = t(spec.key ?? spec.path)
@@ -674,25 +846,25 @@ function escapeHtml(value) {
674
846
  ? jsx('input', {
675
847
  type: 'checkbox', disabled,
676
848
  checked: state.text === 'true',
677
- onChange: (event) => onEdit(event.target.checked ? 'true' : 'false'),
849
+ onChange: (/** @type {{target: {checked: boolean}}} */ event) => onEdit(event.target.checked ? 'true' : 'false'),
678
850
  })
679
851
  : spec.kind === 'choice'
680
852
  ? jsx('select', {
681
853
  className: 'memcard-input', disabled, value: state.text,
682
- onChange: (event) => onEdit(event.target.value),
854
+ onChange: (/** @type {{target: {value: string}}} */ event) => onEdit(event.target.value),
683
855
  }, spec.choices.map((choice) => jsx('option', { key: choice, value: choice }, choice)))
684
856
  : spec.kind === 'policies'
685
857
  ? jsx('textarea', {
686
858
  className: 'memcard-input memcard-textarea', disabled,
687
859
  value: state.text, 'aria-invalid': state.invalid,
688
- onChange: (event) => onEdit(event.target.value),
860
+ onChange: (/** @type {{target: {value: string}}} */ event) => onEdit(event.target.value),
689
861
  })
690
862
  : jsx('input', {
691
863
  type: spec.kind === 'number' ? 'text' : 'text',
692
864
  inputMode: spec.kind === 'number' ? 'numeric' : undefined,
693
865
  className: 'memcard-input', disabled,
694
866
  value: state.text, 'aria-invalid': state.invalid,
695
- onChange: (event) => onEdit(event.target.value),
867
+ onChange: (/** @type {{target: {value: string}}} */ event) => onEdit(event.target.value),
696
868
  })
697
869
  return jsx('div', { className: 'memcard-field' },
698
870
  jsx('div', { className: 'memcard-row' },
@@ -707,10 +879,16 @@ function escapeHtml(value) {
707
879
  )
708
880
  }
709
881
 
710
- /** 设置页组件(settings.section 渲染入口;hooks share: mementoCard → useMementoCard)。 */
882
+ /**
883
+ * 设置页组件(settings.section 渲染入口;hooks share: mementoCard → useMementoCard)。
884
+ * @param {{useMementoCard: (select: (snapshot: any) => any) => any, discard: () => void, save: () => void, edit: (path: string, text: string) => void, base: any}} props - 宿主注入的组件属性。
885
+ * @returns {unknown} react 元素。
886
+ */
711
887
  function MementoSection(props) {
712
888
  const state = props.useMementoCard((snapshot) => snapshot)
713
889
  const language = state.language === 'zh' ? 'zh' : 'en'
890
+ /** 文案取值(缺键回退键名);语言取「所选」语言。 */
891
+ /** @type {(key: string) => string} */
714
892
  const t = (key) => CARD_STRINGS[language][key] ?? key
715
893
  const blocked = !state.dirty || state.saving || state.invalid
716
894
  return jsx('div', { className: 'memsec' },
@@ -732,7 +910,7 @@ function escapeHtml(value) {
732
910
  return jsx(FieldRow, {
733
911
  key: path, t, spec, disabled: !state.writable,
734
912
  state: state.fields[path],
735
- onEdit: (text) => props.edit(path, text),
913
+ onEdit: (/** @type {string} */ text) => props.edit(path, text),
736
914
  onReset: () => {
737
915
  const baseValue = pathValue(props.base, path)
738
916
  props.edit(path, spec.kind === 'policies' ? formatPolicies(baseValue) : formatValue(baseValue))
@@ -746,6 +924,7 @@ function escapeHtml(value) {
746
924
 
747
925
  /** 控制器:scope → 暂存表单 → 设置页快照。保存落盘后同步本页悬浮按钮显隐。 */
748
926
  class MementoCardController {
927
+ /** @param {any} scope - 宿主 settingsScope 句柄(namespace = dsh-memento)。 */
749
928
  constructor(scope) {
750
929
  this.scope = scope
751
930
  this.form = new CardForm(scope, (tops) => {
@@ -767,6 +946,7 @@ function escapeHtml(value) {
767
946
  inject() {
768
947
  return {
769
948
  hooks: { mementoCard: this.store },
949
+ /** @param {string} path - 子字段点路径。 @param {string} text - 草稿文本。 @returns {void} */
770
950
  edit: (path, text) => this.form.stage(path, text),
771
951
  discard: () => this.form.discard(),
772
952
  save: () => { void this.form.save() },
@@ -774,6 +954,11 @@ function escapeHtml(value) {
774
954
  }
775
955
  }
776
956
 
957
+ /**
958
+ * 客户端插件挂载:面板 + 设置页一级项。
959
+ * @param {any} ctx - 宿主客户端上下文(slots/settingsScope)。
960
+ * @returns {void}
961
+ */
777
962
  function apply(ctx) {
778
963
  void bootPanel()
779
964
  if (!styleInstalled && typeof document !== 'undefined') {
@@ -0,0 +1,27 @@
1
+ // client/globals.d.ts — 客户端面的环境声明(浏览器全局 + 宿主平台模块)。
2
+ //
3
+ // 本文件是**脚本**声明(无 import/export):`declare module` 只有在脚本里才是
4
+ // 环境模块声明(在模块文件里会变成"对已存在模块的增强",而本仓并不依赖 react)。
5
+ //
6
+ // client.js 不是 Node 模块:宿主把它当 classic script 注入页面,执行时经
7
+ // window.__ModuleLoader__.load 注册 factory,factory 拿到的 require 是宿主
8
+ // 模块系统(react 等平台内置模块)。这里声明这两件事,让客户端类型门
9
+ // (tsconfig.check.client.json)量到真实契约,而不是靠 any 蒙过去。
10
+
11
+ interface Window {
12
+ /** 宿主注入的模块表:load 注册 factory,同 id 重复 load 会留孤儿工厂。 */
13
+ __ModuleLoader__?: {
14
+ load(entry: { id: string, factory: (require: (id: string) => any) => unknown }): void
15
+ }
16
+ }
17
+
18
+ /**
19
+ * 宿主平台内置的 react。本插件只用 createElement(不引 JSX 编译),
20
+ * 因此这里只声明登记到的那些面;其余用法一律应为类型错误。
21
+ */
22
+ declare module 'react' {
23
+ const react: {
24
+ createElement: (type: unknown, props?: unknown, ...children: unknown[]) => unknown
25
+ }
26
+ export = react
27
+ }