@skillcdn/console 0.4.0 → 0.4.2

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 (308) hide show
  1. package/README.md +20 -2
  2. package/SKILLCDN.md +7 -5
  3. package/dist/api.d.ts +5 -4
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +5 -4
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +579 -45
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts +34 -3
  11. package/dist/cli/format.d.ts.map +1 -1
  12. package/dist/cli/format.js +277 -6
  13. package/dist/cli/format.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +60 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +21 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +374 -0
  20. package/dist/cli/plugin-check.js.map +1 -0
  21. package/dist/cli/plugin-cli.d.ts +20 -0
  22. package/dist/cli/plugin-cli.d.ts.map +1 -0
  23. package/dist/cli/plugin-cli.js +123 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +20 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +74 -0
  28. package/dist/cli/plugin-init.js.map +1 -0
  29. package/dist/cli/plugin-main.d.ts +3 -0
  30. package/dist/cli/plugin-main.d.ts.map +1 -0
  31. package/dist/cli/plugin-main.js +28 -0
  32. package/dist/cli/plugin-main.js.map +1 -0
  33. package/dist/client.d.ts +109 -2
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +67 -2
  36. package/dist/client.js.map +1 -1
  37. package/dist/compat.d.ts +1 -1
  38. package/dist/compat.js +2 -2
  39. package/dist/components/board.d.ts +8 -0
  40. package/dist/components/board.d.ts.map +1 -1
  41. package/dist/components/board.js +59 -15
  42. package/dist/components/board.js.map +1 -1
  43. package/dist/components/event-feed.d.ts +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +33 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/markdown.d.ts.map +1 -1
  48. package/dist/components/markdown.js +6 -3
  49. package/dist/components/markdown.js.map +1 -1
  50. package/dist/components/pack-connect.d.ts +3 -1
  51. package/dist/components/pack-connect.d.ts.map +1 -1
  52. package/dist/components/pack-connect.js +4 -4
  53. package/dist/components/pack-connect.js.map +1 -1
  54. package/dist/components/pack-token.d.ts +3 -1
  55. package/dist/components/pack-token.d.ts.map +1 -1
  56. package/dist/components/pack-token.js +1 -1
  57. package/dist/components/pack-token.js.map +1 -1
  58. package/dist/components/people.d.ts.map +1 -1
  59. package/dist/components/people.js +14 -3
  60. package/dist/components/people.js.map +1 -1
  61. package/dist/components/plugin-frame.d.ts +52 -0
  62. package/dist/components/plugin-frame.d.ts.map +1 -0
  63. package/dist/components/plugin-frame.js +314 -0
  64. package/dist/components/plugin-frame.js.map +1 -0
  65. package/dist/components/plugin-settings.d.ts +38 -0
  66. package/dist/components/plugin-settings.d.ts.map +1 -0
  67. package/dist/components/plugin-settings.js +348 -0
  68. package/dist/components/plugin-settings.js.map +1 -0
  69. package/dist/components/remote-pictures.d.ts +6 -0
  70. package/dist/components/remote-pictures.d.ts.map +1 -0
  71. package/dist/components/remote-pictures.js +8 -0
  72. package/dist/components/remote-pictures.js.map +1 -0
  73. package/dist/components/shell.d.ts +2 -0
  74. package/dist/components/shell.d.ts.map +1 -1
  75. package/dist/components/shell.js +26 -1
  76. package/dist/components/shell.js.map +1 -1
  77. package/dist/components/sign-in.d.ts.map +1 -1
  78. package/dist/components/sign-in.js +49 -2
  79. package/dist/components/sign-in.js.map +1 -1
  80. package/dist/components/ui.d.ts +3 -2
  81. package/dist/components/ui.d.ts.map +1 -1
  82. package/dist/components/ui.js +6 -3
  83. package/dist/components/ui.js.map +1 -1
  84. package/dist/components/workspace.d.ts +7 -1
  85. package/dist/components/workspace.d.ts.map +1 -1
  86. package/dist/components/workspace.js +1 -1
  87. package/dist/components/workspace.js.map +1 -1
  88. package/dist/console.css +384 -5
  89. package/dist/console.d.ts.map +1 -1
  90. package/dist/console.js +146 -29
  91. package/dist/console.js.map +1 -1
  92. package/dist/data.d.ts +11 -2
  93. package/dist/data.d.ts.map +1 -1
  94. package/dist/data.js +35 -21
  95. package/dist/data.js.map +1 -1
  96. package/dist/i18n/en.d.ts +163 -1
  97. package/dist/i18n/en.d.ts.map +1 -1
  98. package/dist/i18n/en.js +174 -1
  99. package/dist/i18n/en.js.map +1 -1
  100. package/dist/i18n/ko.d.ts.map +1 -1
  101. package/dist/i18n/ko.js +154 -1
  102. package/dist/i18n/ko.js.map +1 -1
  103. package/dist/index.d.ts +3 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +3 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/limits.d.ts +14 -0
  108. package/dist/limits.d.ts.map +1 -1
  109. package/dist/limits.js +14 -0
  110. package/dist/limits.js.map +1 -1
  111. package/dist/plugin/args.d.ts +148 -0
  112. package/dist/plugin/args.d.ts.map +1 -0
  113. package/dist/plugin/args.js +369 -0
  114. package/dist/plugin/args.js.map +1 -0
  115. package/dist/plugin/bridge.d.ts +146 -0
  116. package/dist/plugin/bridge.d.ts.map +1 -0
  117. package/dist/plugin/bridge.js +125 -0
  118. package/dist/plugin/bridge.js.map +1 -0
  119. package/dist/plugin/capabilities.d.ts +20 -0
  120. package/dist/plugin/capabilities.d.ts.map +1 -0
  121. package/dist/plugin/capabilities.js +133 -0
  122. package/dist/plugin/capabilities.js.map +1 -0
  123. package/dist/plugin/context.d.ts +374 -0
  124. package/dist/plugin/context.d.ts.map +1 -0
  125. package/dist/plugin/context.js +2 -0
  126. package/dist/plugin/context.js.map +1 -0
  127. package/dist/plugin/define.d.ts +208 -0
  128. package/dist/plugin/define.d.ts.map +1 -0
  129. package/dist/plugin/define.js +482 -0
  130. package/dist/plugin/define.js.map +1 -0
  131. package/dist/plugin/definition-error.d.ts +11 -0
  132. package/dist/plugin/definition-error.d.ts.map +1 -0
  133. package/dist/plugin/definition-error.js +17 -0
  134. package/dist/plugin/definition-error.js.map +1 -0
  135. package/dist/plugin/limits.d.ts +67 -0
  136. package/dist/plugin/limits.d.ts.map +1 -0
  137. package/dist/plugin/limits.js +70 -0
  138. package/dist/plugin/limits.js.map +1 -0
  139. package/dist/plugin/load-worker.d.ts +7 -0
  140. package/dist/plugin/load-worker.d.ts.map +1 -0
  141. package/dist/plugin/load-worker.js +41 -0
  142. package/dist/plugin/load-worker.js.map +1 -0
  143. package/dist/plugin/load.d.ts +18 -0
  144. package/dist/plugin/load.d.ts.map +1 -0
  145. package/dist/plugin/load.js +62 -0
  146. package/dist/plugin/load.js.map +1 -0
  147. package/dist/plugin/manifest.d.ts +237 -0
  148. package/dist/plugin/manifest.d.ts.map +1 -0
  149. package/dist/plugin/manifest.js +411 -0
  150. package/dist/plugin/manifest.js.map +1 -0
  151. package/dist/plugin/refusals.d.ts +49 -0
  152. package/dist/plugin/refusals.d.ts.map +1 -0
  153. package/dist/plugin/refusals.js +79 -0
  154. package/dist/plugin/refusals.js.map +1 -0
  155. package/dist/plugin/rest-schemas.d.ts +556 -0
  156. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  157. package/dist/plugin/rest-schemas.js +262 -0
  158. package/dist/plugin/rest-schemas.js.map +1 -0
  159. package/dist/plugin/sandbox.d.ts +23 -0
  160. package/dist/plugin/sandbox.d.ts.map +1 -0
  161. package/dist/plugin/sandbox.js +99 -0
  162. package/dist/plugin/sandbox.js.map +1 -0
  163. package/dist/plugin/testing.d.ts +45 -0
  164. package/dist/plugin/testing.d.ts.map +1 -0
  165. package/dist/plugin/testing.js +420 -0
  166. package/dist/plugin/testing.js.map +1 -0
  167. package/dist/plugin/web-react.d.ts +11 -0
  168. package/dist/plugin/web-react.d.ts.map +1 -0
  169. package/dist/plugin/web-react.js +34 -0
  170. package/dist/plugin/web-react.js.map +1 -0
  171. package/dist/plugin/web.d.ts +95 -0
  172. package/dist/plugin/web.d.ts.map +1 -0
  173. package/dist/plugin/web.js +245 -0
  174. package/dist/plugin/web.js.map +1 -0
  175. package/dist/plugin-nav.d.ts +29 -0
  176. package/dist/plugin-nav.d.ts.map +1 -0
  177. package/dist/plugin-nav.js +92 -0
  178. package/dist/plugin-nav.js.map +1 -0
  179. package/dist/plugin-testing.d.ts +2 -0
  180. package/dist/plugin-testing.d.ts.map +1 -0
  181. package/dist/plugin-testing.js +4 -0
  182. package/dist/plugin-testing.js.map +1 -0
  183. package/dist/plugin-web-react.d.ts +3 -0
  184. package/dist/plugin-web-react.d.ts.map +1 -0
  185. package/dist/plugin-web-react.js +3 -0
  186. package/dist/plugin-web-react.js.map +1 -0
  187. package/dist/plugin-web.d.ts +4 -0
  188. package/dist/plugin-web.d.ts.map +1 -0
  189. package/dist/plugin-web.js +5 -0
  190. package/dist/plugin-web.js.map +1 -0
  191. package/dist/plugin.d.ts +11 -0
  192. package/dist/plugin.d.ts.map +1 -0
  193. package/dist/plugin.js +15 -0
  194. package/dist/plugin.js.map +1 -0
  195. package/dist/router.d.ts +28 -0
  196. package/dist/router.d.ts.map +1 -1
  197. package/dist/router.js +47 -0
  198. package/dist/router.js.map +1 -1
  199. package/dist/routes.d.ts +6 -0
  200. package/dist/routes.d.ts.map +1 -1
  201. package/dist/routes.js +8 -0
  202. package/dist/routes.js.map +1 -1
  203. package/dist/schemas.d.ts +147 -1
  204. package/dist/schemas.d.ts.map +1 -1
  205. package/dist/schemas.js +55 -5
  206. package/dist/schemas.js.map +1 -1
  207. package/dist/tokens.d.ts +2 -0
  208. package/dist/tokens.d.ts.map +1 -0
  209. package/dist/tokens.js +101 -0
  210. package/dist/tokens.js.map +1 -0
  211. package/dist/vocabulary.d.ts +11 -1
  212. package/dist/vocabulary.d.ts.map +1 -1
  213. package/dist/vocabulary.js +24 -0
  214. package/dist/vocabulary.js.map +1 -1
  215. package/dist/web.d.ts.map +1 -1
  216. package/dist/web.js +5 -2
  217. package/dist/web.js.map +1 -1
  218. package/docs/specs/cli.md +16 -9
  219. package/docs/specs/plugins.md +487 -0
  220. package/docs/specs/rest.md +25 -13
  221. package/package.json +21 -3
  222. package/skills/building-a-console/SKILL.md +25 -26
  223. package/skills/building-a-console/references/configuring.md +40 -0
  224. package/skills/building-a-console/references/the-default-console.md +1 -1
  225. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  226. package/skills/building-a-plugin/SKILL.md +137 -0
  227. package/skills/deploying-a-console/SKILL.md +24 -26
  228. package/skills/deploying-a-console/references/access.md +13 -9
  229. package/skills/deploying-a-console/references/resources.md +25 -10
  230. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  231. package/skills/deploying-a-console/references/sizing.md +2 -0
  232. package/skills/planning-the-board/SKILL.md +96 -0
  233. package/skills/planning-the-board/references/a-task.md +36 -0
  234. package/skills/planning-the-board/references/setting-up.md +44 -0
  235. package/skills/working-the-board/SKILL.md +43 -34
  236. package/skills/working-the-board/references/the-pack.md +19 -0
  237. package/src/api.ts +16 -0
  238. package/src/cli/cli.ts +736 -45
  239. package/src/cli/format.ts +356 -9
  240. package/src/cli/help.ts +71 -6
  241. package/src/cli/plugin-check.ts +470 -0
  242. package/src/cli/plugin-cli.ts +148 -0
  243. package/src/cli/plugin-init.ts +90 -0
  244. package/src/cli/plugin-main.ts +30 -0
  245. package/src/client.ts +324 -1
  246. package/src/compat.ts +2 -2
  247. package/src/components/board.tsx +62 -14
  248. package/src/components/event-feed.tsx +42 -1
  249. package/src/components/markdown.tsx +6 -3
  250. package/src/components/pack-connect.tsx +10 -7
  251. package/src/components/pack-token.tsx +4 -1
  252. package/src/components/people.tsx +17 -7
  253. package/src/components/plugin-frame.tsx +437 -0
  254. package/src/components/plugin-settings.tsx +1111 -0
  255. package/src/components/remote-pictures.ts +19 -0
  256. package/src/components/shell.tsx +82 -14
  257. package/src/components/sign-in.tsx +114 -13
  258. package/src/components/ui.tsx +6 -3
  259. package/src/components/workspace.tsx +18 -0
  260. package/src/console.tsx +309 -23
  261. package/src/data.ts +47 -22
  262. package/src/i18n/en.ts +183 -1
  263. package/src/i18n/ko.ts +159 -1
  264. package/src/index.ts +12 -0
  265. package/src/limits.ts +14 -0
  266. package/src/plugin/args.ts +526 -0
  267. package/src/plugin/bridge.ts +314 -0
  268. package/src/plugin/capabilities.ts +150 -0
  269. package/src/plugin/context.ts +416 -0
  270. package/src/plugin/define.ts +792 -0
  271. package/src/plugin/definition-error.ts +19 -0
  272. package/src/plugin/limits.ts +70 -0
  273. package/src/plugin/load-worker.ts +50 -0
  274. package/src/plugin/load.ts +82 -0
  275. package/src/plugin/manifest.ts +561 -0
  276. package/src/plugin/refusals.ts +94 -0
  277. package/src/plugin/rest-schemas.ts +354 -0
  278. package/src/plugin/sandbox.ts +123 -0
  279. package/src/plugin/testing.ts +612 -0
  280. package/src/plugin/web-react.ts +45 -0
  281. package/src/plugin/web.ts +403 -0
  282. package/src/plugin-nav.ts +132 -0
  283. package/src/plugin-testing.ts +10 -0
  284. package/src/plugin-web-react.ts +4 -0
  285. package/src/plugin-web.ts +35 -0
  286. package/src/plugin.ts +126 -0
  287. package/src/router.ts +73 -0
  288. package/src/routes.ts +9 -0
  289. package/src/schemas.ts +60 -4
  290. package/src/styles/console.css +384 -5
  291. package/src/tokens.ts +101 -0
  292. package/src/vocabulary.ts +30 -0
  293. package/src/web.ts +5 -2
  294. package/template/plugin/.gitignore +2 -0
  295. package/template/plugin/README.md +26 -0
  296. package/template/plugin/SKILLCDN.md +13 -0
  297. package/template/plugin/build.mjs +40 -0
  298. package/template/plugin/console-plugin.json +50 -0
  299. package/template/plugin/package.json +27 -0
  300. package/template/plugin/skills/__key__/SKILL.md +23 -0
  301. package/template/plugin/src/check.test.ts +34 -0
  302. package/template/plugin/src/server/index.test.ts +41 -0
  303. package/template/plugin/src/server/index.ts +72 -0
  304. package/template/plugin/src/web/index.css +51 -0
  305. package/template/plugin/src/web/index.ts +115 -0
  306. package/template/plugin/tsconfig.json +17 -0
  307. package/skills/working-the-board/references/setting-up.md +0 -3
  308. /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
@@ -0,0 +1,403 @@
1
+ // The frame's side of the bridge (docs/specs/plugins.md, "The bridge"): what a plugin's page
2
+ // bundles to speak to the console's page. It depends on nothing, speaks to the origin the frame
3
+ // document names alone, performs its plugin's own calls and nothing else, applies the console's
4
+ // tokens to the document, and says its height. A framework is the plugin's choice; this is below
5
+ // any.
6
+ import {
7
+ BRIDGE_METHODS,
8
+ type BridgeCommandResult,
9
+ type BridgeError,
10
+ type BridgeEventKind,
11
+ type BridgeFile,
12
+ type BridgeInit,
13
+ type BridgeMethod,
14
+ type BridgeToPage,
15
+ isBridgePath,
16
+ isBridgeToFrame,
17
+ MAX_BRIDGE_TITLE_LENGTH,
18
+ PLUGIN_BRIDGE_VERSION,
19
+ } from "./bridge.js";
20
+ import type { PluginRecord, RecordPage, RecordVersion } from "./context.js";
21
+
22
+ /** What the console told the frame: who looks, at what, in which words. */
23
+ export type PluginFrameContext = Omit<BridgeInit, "v" | "t">;
24
+
25
+ /** A refusal from the console, with its stable code, as the page relayed it. */
26
+ export class PluginBridgeError extends Error {
27
+ readonly code: string;
28
+ readonly data: unknown;
29
+
30
+ constructor(error: BridgeError) {
31
+ super(error.message);
32
+ this.name = "PluginBridgeError";
33
+ this.code = error.code;
34
+ this.data = error.data;
35
+ }
36
+ }
37
+
38
+ export interface FrameMessageEvent {
39
+ readonly data: unknown;
40
+ readonly origin: string;
41
+ readonly source: unknown;
42
+ }
43
+
44
+ /** As much of `window` as the bridge uses; a test hands one of its own. */
45
+ export interface FrameWindow {
46
+ readonly parent: {
47
+ postMessage(message: unknown, targetOrigin: string, transfer?: Transferable[]): void;
48
+ };
49
+ addEventListener(type: "message", listener: (event: FrameMessageEvent) => void): void;
50
+ removeEventListener(type: "message", listener: (event: FrameMessageEvent) => void): void;
51
+ }
52
+
53
+ /** As much of `document` as the bridge uses. */
54
+ export interface FrameDocument {
55
+ readonly documentElement: {
56
+ readonly dataset: Readonly<Record<string, string | undefined>>;
57
+ readonly style: { setProperty(name: string, value: string): void };
58
+ lang: string;
59
+ };
60
+ readonly body: { readonly scrollHeight: number } | null;
61
+ title: string;
62
+ }
63
+
64
+ export interface ConnectOptions {
65
+ readonly window?: FrameWindow;
66
+ readonly document?: FrameDocument;
67
+ /** How long to wait for the console's `init`; ten seconds unless said. */
68
+ readonly timeoutMs?: number;
69
+ /** Whether the frame says its height as it changes; on unless said otherwise. */
70
+ readonly resize?: boolean;
71
+ }
72
+
73
+ export interface FrameRecordsQuery {
74
+ readonly where?: Readonly<Record<string, unknown>>;
75
+ /** A field of the collection's indexes, `-` before it for descending. */
76
+ readonly order?: string;
77
+ readonly limit?: number;
78
+ readonly after?: string;
79
+ readonly q?: string;
80
+ readonly project?: string;
81
+ }
82
+
83
+ /** The frame's connection to the console's page. */
84
+ export interface PluginFrame {
85
+ readonly context: PluginFrameContext;
86
+ /** A call as the bridge carries one; the typed methods below are the usual way. */
87
+ call(method: BridgeMethod, params: Readonly<Record<string, unknown>>): Promise<unknown>;
88
+ command(name: string, args?: Readonly<Record<string, unknown>>): Promise<BridgeCommandResult>;
89
+ query(name: string, args?: Readonly<Record<string, unknown>>): Promise<unknown>;
90
+ readonly records: {
91
+ list<T = Record<string, unknown>>(
92
+ collection: string,
93
+ query?: FrameRecordsQuery,
94
+ ): Promise<RecordPage<T>>;
95
+ get<T = Record<string, unknown>>(
96
+ collection: string,
97
+ id: string,
98
+ version?: number,
99
+ ): Promise<PluginRecord<T>>;
100
+ versions<T = Record<string, unknown>>(
101
+ collection: string,
102
+ id: string,
103
+ ): Promise<readonly RecordVersion<T>[]>;
104
+ };
105
+ readonly files: {
106
+ get(id: string): Promise<BridgeFile>;
107
+ /** Uploads bytes for a command; answers the handle, good for an hour. */
108
+ put(
109
+ bytes: ArrayBuffer | Uint8Array,
110
+ name: string,
111
+ type: string,
112
+ ): Promise<{ readonly handle: string }>;
113
+ };
114
+ /** The console's page follows a path of the console's in place; a URL is refused. */
115
+ navigate(path: string): void;
116
+ /** The console's page downloads a file of this plugin. */
117
+ download(id: string): void;
118
+ setTitle(text: string): void;
119
+ /** Something of this plugin changed; subscribes to the kinds named. The function returned stops listening. */
120
+ onEvent(
121
+ kinds: readonly BridgeEventKind[],
122
+ handler: (kind: BridgeEventKind, data: unknown) => void,
123
+ ): () => void;
124
+ /** The console's look changed; the tokens are applied before the handler runs. */
125
+ onTheme(handler: (theme: Readonly<Record<string, string>>) => void): () => void;
126
+ /** The console navigated within this page's address. */
127
+ onPath(handler: (path: string) => void): () => void;
128
+ /** Stops listening to the console's page. */
129
+ close(): void;
130
+ }
131
+
132
+ const DEFAULT_CONNECT_MS = 10_000;
133
+
134
+ function globalWindow(): FrameWindow {
135
+ const candidate = (globalThis as { window?: FrameWindow }).window;
136
+ if (candidate === undefined) {
137
+ throw new PluginBridgeError({
138
+ code: "plugin.no_window",
139
+ message: "connect() runs in a plugin's frame, where window is",
140
+ });
141
+ }
142
+ return candidate;
143
+ }
144
+
145
+ function globalDocument(): FrameDocument {
146
+ const candidate = (globalThis as { document?: FrameDocument }).document;
147
+ if (candidate === undefined) {
148
+ throw new PluginBridgeError({
149
+ code: "plugin.no_window",
150
+ message: "connect() runs in a plugin's frame, where document is",
151
+ });
152
+ }
153
+ return candidate;
154
+ }
155
+
156
+ /** Sets the console's tokens on the document's root, `--sc-*` and nothing else. */
157
+ export function applyTheme(document: FrameDocument, theme: Readonly<Record<string, string>>): void {
158
+ for (const [token, value] of Object.entries(theme)) {
159
+ if (token.startsWith("--sc-")) {
160
+ document.documentElement.style.setProperty(token, value);
161
+ }
162
+ }
163
+ }
164
+
165
+ function startHeightWatch(document: FrameDocument, post: (height: number) => void): () => void {
166
+ const body = document.body;
167
+ if (body === null) {
168
+ return () => {};
169
+ }
170
+ let last = -1;
171
+ const say = () => {
172
+ const height = Math.ceil(body.scrollHeight);
173
+ if (height !== last) {
174
+ last = height;
175
+ post(height);
176
+ }
177
+ };
178
+ say();
179
+ const observerClass = (
180
+ globalThis as {
181
+ ResizeObserver?: new (
182
+ callback: () => void,
183
+ ) => { observe(target: unknown): void; disconnect(): void };
184
+ }
185
+ ).ResizeObserver;
186
+ const elementClass = (globalThis as { Element?: abstract new () => unknown }).Element;
187
+ if (observerClass !== undefined && elementClass !== undefined && body instanceof elementClass) {
188
+ const observer = new observerClass(say);
189
+ observer.observe(body);
190
+ return () => observer.disconnect();
191
+ }
192
+ const timer = setInterval(say, 500);
193
+ return () => clearInterval(timer);
194
+ }
195
+
196
+ /**
197
+ * Connects the frame to the console's page: says `ready`, waits for `init`, applies the tokens,
198
+ * and answers the connection. Call it once, when the bundle starts.
199
+ */
200
+ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
201
+ const win = options.window ?? globalWindow();
202
+ const doc = options.document ?? globalDocument();
203
+ const origin = doc.documentElement.dataset.console;
204
+ if (origin === undefined || origin.length === 0) {
205
+ return Promise.reject(
206
+ new PluginBridgeError({
207
+ code: "plugin.no_console",
208
+ message: "the document names no console in data-console; this is not a plugin's frame",
209
+ }),
210
+ );
211
+ }
212
+ const post = (message: BridgeToPage, transfer?: Transferable[]) => {
213
+ win.parent.postMessage(message, origin, transfer);
214
+ };
215
+ const pending = new Map<
216
+ string,
217
+ { resolve: (value: unknown) => void; reject: (error: Error) => void }
218
+ >();
219
+ const eventHandlers = new Set<(kind: BridgeEventKind, data: unknown) => void>();
220
+ const themeHandlers = new Set<(theme: Readonly<Record<string, string>>) => void>();
221
+ const pathHandlers = new Set<(path: string) => void>();
222
+ const subscribed = new Set<BridgeEventKind>();
223
+ let counter = 0;
224
+ let stopHeight: (() => void) | undefined;
225
+
226
+ return new Promise<PluginFrame>((resolveConnect, rejectConnect) => {
227
+ let connected = false;
228
+ const timer = setTimeout(() => {
229
+ if (!connected) {
230
+ win.removeEventListener("message", listener);
231
+ rejectConnect(
232
+ new PluginBridgeError({
233
+ code: "plugin.no_init",
234
+ message: `the console's page did not answer within ${options.timeoutMs ?? DEFAULT_CONNECT_MS} ms`,
235
+ }),
236
+ );
237
+ }
238
+ }, options.timeoutMs ?? DEFAULT_CONNECT_MS);
239
+
240
+ const call = (
241
+ method: BridgeMethod,
242
+ params: Readonly<Record<string, unknown>>,
243
+ transfer?: Transferable[],
244
+ ): Promise<unknown> => {
245
+ if (!(BRIDGE_METHODS as readonly string[]).includes(method)) {
246
+ return Promise.reject(
247
+ new PluginBridgeError({
248
+ code: "plugin.forbidden",
249
+ message: `${method} is not a method of the bridge`,
250
+ }),
251
+ );
252
+ }
253
+ counter += 1;
254
+ const id = String(counter);
255
+ return new Promise((resolve, reject) => {
256
+ pending.set(id, { resolve, reject });
257
+ post({ v: PLUGIN_BRIDGE_VERSION, t: "call", id, method, params }, transfer);
258
+ });
259
+ };
260
+
261
+ const listener = (event: FrameMessageEvent) => {
262
+ if (event.source !== win.parent || event.origin !== origin || !isBridgeToFrame(event.data)) {
263
+ return;
264
+ }
265
+ const message = event.data;
266
+ switch (message.t) {
267
+ case "init": {
268
+ if (connected) {
269
+ return;
270
+ }
271
+ connected = true;
272
+ clearTimeout(timer);
273
+ applyTheme(doc, message.theme);
274
+ doc.documentElement.lang = message.language;
275
+ const { v: _v, t: _t, ...context } = message;
276
+ const frame: PluginFrame = {
277
+ context,
278
+ call: (method, params) => call(method, params),
279
+ command: (name, args = {}) =>
280
+ call("command", { name, args }) as Promise<BridgeCommandResult>,
281
+ query: (name, args = {}) => call("query", { name, args }),
282
+ records: {
283
+ list: <T>(collection: string, query: FrameRecordsQuery = {}) =>
284
+ call("records.list", { collection, ...query }) as Promise<RecordPage<T>>,
285
+ get: <T>(collection: string, id: string, version?: number) =>
286
+ call(
287
+ "records.get",
288
+ version === undefined ? { collection, id } : { collection, id, version },
289
+ ) as Promise<PluginRecord<T>>,
290
+ versions: <T>(collection: string, id: string) =>
291
+ call("records.versions", { collection, id }) as Promise<
292
+ readonly RecordVersion<T>[]
293
+ >,
294
+ },
295
+ files: {
296
+ get: (id) => call("files.get", { id }) as Promise<BridgeFile>,
297
+ put: (bytes, name, type) => {
298
+ const buffer = bytes instanceof Uint8Array ? bytes.slice().buffer : bytes;
299
+ return call("files.put", { bytes: buffer, name, type }, [buffer]) as Promise<{
300
+ handle: string;
301
+ }>;
302
+ },
303
+ },
304
+ navigate: (path) => {
305
+ if (!isBridgePath(path)) {
306
+ throw new PluginBridgeError({
307
+ code: "plugin.forbidden",
308
+ message: "navigate takes a path of the console's, never a URL",
309
+ });
310
+ }
311
+ post({ v: PLUGIN_BRIDGE_VERSION, t: "navigate", path });
312
+ },
313
+ download: (id) => post({ v: PLUGIN_BRIDGE_VERSION, t: "download", id }),
314
+ setTitle: (text) => {
315
+ const title = text.slice(0, MAX_BRIDGE_TITLE_LENGTH).replace(/[\r\n]+/g, " ");
316
+ doc.title = title;
317
+ post({ v: PLUGIN_BRIDGE_VERSION, t: "title", text: title });
318
+ },
319
+ onEvent: (kinds, handler) => {
320
+ eventHandlers.add(handler);
321
+ let added = false;
322
+ for (const kind of kinds) {
323
+ if (!subscribed.has(kind)) {
324
+ subscribed.add(kind);
325
+ added = true;
326
+ }
327
+ }
328
+ if (added) {
329
+ post({ v: PLUGIN_BRIDGE_VERSION, t: "subscribe", kinds: [...subscribed] });
330
+ }
331
+ return () => eventHandlers.delete(handler);
332
+ },
333
+ onTheme: (handler) => {
334
+ themeHandlers.add(handler);
335
+ return () => themeHandlers.delete(handler);
336
+ },
337
+ onPath: (handler) => {
338
+ pathHandlers.add(handler);
339
+ return () => pathHandlers.delete(handler);
340
+ },
341
+ close: () => {
342
+ win.removeEventListener("message", listener);
343
+ stopHeight?.();
344
+ for (const { reject } of pending.values()) {
345
+ reject(
346
+ new PluginBridgeError({
347
+ code: "plugin.closed",
348
+ message: "the frame closed its connection",
349
+ }),
350
+ );
351
+ }
352
+ pending.clear();
353
+ },
354
+ };
355
+ if (options.resize !== false) {
356
+ stopHeight = startHeightWatch(doc, (height) =>
357
+ post({ v: PLUGIN_BRIDGE_VERSION, t: "resize", height }),
358
+ );
359
+ }
360
+ resolveConnect(frame);
361
+ return;
362
+ }
363
+ case "result": {
364
+ const waiting = pending.get(message.id);
365
+ if (waiting === undefined) {
366
+ return;
367
+ }
368
+ pending.delete(message.id);
369
+ if (message.ok) {
370
+ waiting.resolve(message.value);
371
+ } else {
372
+ waiting.reject(
373
+ new PluginBridgeError(
374
+ message.error ?? { code: "plugin.failed", message: "the call failed" },
375
+ ),
376
+ );
377
+ }
378
+ return;
379
+ }
380
+ case "event":
381
+ for (const handler of eventHandlers) {
382
+ handler(message.kind, message.data);
383
+ }
384
+ return;
385
+ case "theme":
386
+ applyTheme(doc, message.theme);
387
+ for (const handler of themeHandlers) {
388
+ handler(message.theme);
389
+ }
390
+ return;
391
+ case "path":
392
+ for (const handler of pathHandlers) {
393
+ handler(message.path);
394
+ }
395
+ return;
396
+ default:
397
+ return;
398
+ }
399
+ };
400
+ win.addEventListener("message", listener);
401
+ post({ v: PLUGIN_BRIDGE_VERSION, t: "ready" });
402
+ });
403
+ }
@@ -0,0 +1,132 @@
1
+ import type { NavItem } from "./components/shell.js";
2
+ import type { Messages } from "./i18n/en.js";
3
+ import { pluginHref } from "./router.js";
4
+ import type { RestPlugin, RestRulesPlugin } from "./schemas.js";
5
+
6
+ // The tabs of the plugins (ADR-0054, docs/specs/plugins.md, "Tabs, sub-tabs and addresses"):
7
+ // after the console's own tabs, the groups that hold a plugin the person has a role in. A group
8
+ // with one plugin is a tab with the plugin's name; one with several is a tab with the group's
9
+ // name that opens the list of them. Groups and plugins are ordered by `menu.order`, then by key.
10
+ // Pure: what the tabs are, from what the console was told, for the composition and its tests.
11
+
12
+ /** A text in the person's language, else in English, else the first the manifest has. */
13
+ export function pluginWordOf(
14
+ texts: Readonly<Record<string, string>> | null | undefined,
15
+ language: string,
16
+ ): string {
17
+ if (texts === null || texts === undefined) {
18
+ return "";
19
+ }
20
+ return (
21
+ texts[language] ??
22
+ texts[language.split("-")[0] ?? language] ??
23
+ texts.en ??
24
+ Object.values(texts)[0] ??
25
+ ""
26
+ );
27
+ }
28
+
29
+ /** The pages of a plugin at a level, in the manifest's order: what the person may see of them. */
30
+ export function pluginPagesAt(
31
+ plugin: RestPlugin,
32
+ level: "workspace" | "project",
33
+ ): RestPlugin["pages"] {
34
+ return plugin.pages.filter((page) => page.level === level);
35
+ }
36
+
37
+ /** Where a plugin's tab leads: its first page at the level. */
38
+ function firstPageHref(
39
+ plugin: RestPlugin,
40
+ level: "workspace" | "project",
41
+ project: string | undefined,
42
+ ): string | undefined {
43
+ const [first] = pluginPagesAt(plugin, level);
44
+ return first === undefined ? undefined : pluginHref(plugin.key, first.key, project);
45
+ }
46
+
47
+ /** What the current address names, for the tabs to say which is current. */
48
+ export interface PluginRouteFacts {
49
+ readonly plugin: string | undefined;
50
+ }
51
+
52
+ /**
53
+ * The tabs of the plugins at a level: on the workspace, every enabled plugin with a page there the
54
+ * person may see; on a project, those bound to it (as the project's rules name them) with a
55
+ * project page. Grouped by `menu.group`, named in the person's language.
56
+ */
57
+ export function pluginNavItems(input: {
58
+ readonly plugins: readonly RestPlugin[];
59
+ readonly level: "workspace" | "project";
60
+ /** The project the tabs are on, with the plugins bound to it; nothing on the workspace. */
61
+ readonly project?:
62
+ | { readonly key: string; readonly plugins: readonly RestRulesPlugin[] }
63
+ | undefined;
64
+ readonly current: PluginRouteFacts;
65
+ readonly words: Messages;
66
+ readonly language: string;
67
+ }): NavItem[] {
68
+ const { plugins, level, project, current, words, language } = input;
69
+ const bound = project === undefined ? undefined : new Set(project.plugins.map((p) => p.key));
70
+ const shown = plugins
71
+ .filter((plugin) => plugin.state === "enabled")
72
+ .filter((plugin) => bound === undefined || bound.has(plugin.key))
73
+ .filter((plugin) => pluginPagesAt(plugin, level).length > 0);
74
+ const groups = new Map<string, { order: number; name: string; plugins: RestPlugin[] }>();
75
+ for (const plugin of shown) {
76
+ const group = plugin.menu?.group ?? "operations";
77
+ const known = words.nav.pluginGroups[group];
78
+ const name =
79
+ known ??
80
+ (plugin.menu?.name === null || plugin.menu?.name === undefined
81
+ ? group
82
+ : pluginWordOf(plugin.menu.name, language));
83
+ const order = plugin.menu?.order ?? 100;
84
+ const held = groups.get(group);
85
+ if (held === undefined) {
86
+ groups.set(group, { order, name, plugins: [plugin] });
87
+ } else {
88
+ held.plugins.push(plugin);
89
+ held.order = Math.min(held.order, order);
90
+ }
91
+ }
92
+ const byOrder = (a: { order: number; key: string }, b: { order: number; key: string }) =>
93
+ a.order - b.order || a.key.localeCompare(b.key);
94
+ return [...groups.entries()]
95
+ .map(([key, group]) => ({ key, ...group }))
96
+ .sort(byOrder)
97
+ .flatMap((group) => {
98
+ const members = [...group.plugins].sort((a, b) =>
99
+ byOrder(
100
+ { order: a.menu?.order ?? 100, key: a.key },
101
+ { order: b.menu?.order ?? 100, key: b.key },
102
+ ),
103
+ );
104
+ const items = members.flatMap((plugin): NavItem[] => {
105
+ const href = firstPageHref(plugin, level, project?.key);
106
+ return href === undefined
107
+ ? []
108
+ : [
109
+ {
110
+ href,
111
+ label: pluginWordOf(plugin.name, language),
112
+ current: current.plugin === plugin.key,
113
+ },
114
+ ];
115
+ });
116
+ if (items.length === 0) {
117
+ return [];
118
+ }
119
+ if (items.length === 1) {
120
+ return items;
121
+ }
122
+ const first = items[0] as NavItem;
123
+ return [
124
+ {
125
+ href: first.href,
126
+ label: group.name,
127
+ current: items.some((item) => item.current),
128
+ children: items,
129
+ },
130
+ ];
131
+ });
132
+ }
@@ -0,0 +1,10 @@
1
+ // A context for a plugin's own tests: records in memory, the console refusing what the test did
2
+ // not provide, everything emitted and logged kept for assertions.
3
+
4
+ export {
5
+ createTestContext,
6
+ type LoggedLine,
7
+ type TestConsoleOptions,
8
+ type TestContext,
9
+ type TestContextOptions,
10
+ } from "./plugin/testing.js";
@@ -0,0 +1,4 @@
1
+ // The bridge as a React hook, for a plugin that chooses React: `usePlugin()`.
2
+
3
+ export type { PluginFrame, PluginFrameContext } from "./plugin/web.js";
4
+ export { sharedConnection, type UsePlugin, usePlugin } from "./plugin/web-react.js";
@@ -0,0 +1,35 @@
1
+ // The frame's side of the bridge (docs/specs/plugins.md, "The bridge"), for a plugin's web bundle:
2
+ // `connect()` and the messages. Depends on nothing; runs in a browser.
3
+
4
+ export {
5
+ BRIDGE_EVENT_KINDS,
6
+ BRIDGE_METHODS,
7
+ type BridgeCommandResult,
8
+ type BridgeError,
9
+ type BridgeEventKind,
10
+ type BridgeFile,
11
+ type BridgeInit,
12
+ type BridgeMethod,
13
+ type BridgePerson,
14
+ type BridgeToFrame,
15
+ type BridgeToPage,
16
+ isBridgePath,
17
+ isBridgeToFrame,
18
+ isBridgeToPage,
19
+ MAX_BRIDGE_PATH_LENGTH,
20
+ MAX_BRIDGE_TITLE_LENGTH,
21
+ PLUGIN_BRIDGE_VERSION,
22
+ } from "./plugin/bridge.js";
23
+ export type { LinkTarget, PluginRecord, RecordPage, RecordVersion } from "./plugin/context.js";
24
+ export {
25
+ applyTheme,
26
+ type ConnectOptions,
27
+ connect,
28
+ type FrameDocument,
29
+ type FrameMessageEvent,
30
+ type FrameRecordsQuery,
31
+ type FrameWindow,
32
+ PluginBridgeError,
33
+ type PluginFrame,
34
+ type PluginFrameContext,
35
+ } from "./plugin/web.js";