react-pebble 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/lib/compiler.cjs +3 -0
  2. package/dist/lib/compiler.cjs.map +1 -0
  3. package/dist/lib/compiler.js +54 -0
  4. package/dist/lib/compiler.js.map +1 -0
  5. package/dist/lib/components.cjs +2 -0
  6. package/dist/lib/components.cjs.map +1 -0
  7. package/dist/lib/components.js +80 -0
  8. package/dist/lib/components.js.map +1 -0
  9. package/dist/lib/hooks.cjs +2 -0
  10. package/dist/lib/hooks.cjs.map +1 -0
  11. package/dist/lib/hooks.js +99 -0
  12. package/dist/lib/hooks.js.map +1 -0
  13. package/dist/lib/index.cjs +2 -0
  14. package/dist/lib/index.cjs.map +1 -0
  15. package/dist/lib/index.js +585 -0
  16. package/dist/lib/index.js.map +1 -0
  17. package/dist/lib/platform.cjs +2 -0
  18. package/dist/lib/platform.cjs.map +1 -0
  19. package/dist/lib/platform.js +52 -0
  20. package/dist/lib/platform.js.map +1 -0
  21. package/dist/lib/plugin.cjs +60 -0
  22. package/dist/lib/plugin.cjs.map +1 -0
  23. package/dist/lib/plugin.js +102 -0
  24. package/dist/lib/plugin.js.map +1 -0
  25. package/dist/lib/src/compiler/index.d.ts +40 -0
  26. package/dist/lib/src/components/index.d.ts +129 -0
  27. package/dist/lib/src/hooks/index.d.ts +75 -0
  28. package/dist/lib/src/index.d.ts +36 -0
  29. package/dist/lib/src/pebble-dom-shim.d.ts +45 -0
  30. package/dist/lib/src/pebble-dom.d.ts +59 -0
  31. package/dist/lib/src/pebble-output.d.ts +44 -0
  32. package/dist/lib/src/pebble-reconciler.d.ts +16 -0
  33. package/dist/lib/src/pebble-render.d.ts +31 -0
  34. package/dist/lib/src/platform.d.ts +30 -0
  35. package/dist/lib/src/plugin/index.d.ts +20 -0
  36. package/package.json +90 -0
  37. package/scripts/compile-to-piu.ts +1794 -0
  38. package/scripts/deploy.sh +46 -0
  39. package/src/compiler/index.ts +114 -0
  40. package/src/components/index.tsx +280 -0
  41. package/src/hooks/index.ts +311 -0
  42. package/src/index.ts +126 -0
  43. package/src/pebble-dom-shim.ts +266 -0
  44. package/src/pebble-dom.ts +190 -0
  45. package/src/pebble-output.ts +310 -0
  46. package/src/pebble-reconciler.ts +54 -0
  47. package/src/pebble-render.ts +311 -0
  48. package/src/platform.ts +50 -0
  49. package/src/plugin/index.ts +274 -0
  50. package/src/types/moddable.d.ts +156 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * pebble-dom.ts — Virtual DOM layer for React Pebble
3
+ *
4
+ * Modeled after Ink's dom.ts but stripped of Yoga layout.
5
+ * Pebble uses absolute positioning (x, y, w, h) rather than flexbox,
6
+ * so nodes are just plain JS objects with type, props, and children.
7
+ *
8
+ * Each node type maps to a Pebble drawing primitive:
9
+ * pbl-root → Container root (the Window)
10
+ * pbl-rect → fillRect / drawRect
11
+ * pbl-circle → fillCircle / drawCircle
12
+ * pbl-text → drawText
13
+ * pbl-line → drawLine
14
+ * pbl-image → drawImage (bitmap)
15
+ * pbl-group → Logical grouping with offset (no draw call)
16
+ * #text → Raw text content (only valid inside pbl-text)
17
+ */
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Element types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export type ElementType =
24
+ | 'pbl-root'
25
+ | 'pbl-rect'
26
+ | 'pbl-circle'
27
+ | 'pbl-text'
28
+ | 'pbl-line'
29
+ | 'pbl-image'
30
+ | 'pbl-group'
31
+ | 'pbl-statusbar'
32
+ | 'pbl-actionbar';
33
+
34
+ export const ELEMENT_TYPES: ReadonlySet<ElementType> = new Set<ElementType>([
35
+ 'pbl-root',
36
+ 'pbl-rect',
37
+ 'pbl-circle',
38
+ 'pbl-text',
39
+ 'pbl-line',
40
+ 'pbl-image',
41
+ 'pbl-group',
42
+ 'pbl-statusbar',
43
+ 'pbl-actionbar',
44
+ ]);
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Node shapes
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * Loose prop bag — element-specific shapes are documented on each component
52
+ * wrapper, but the reconciler treats them uniformly.
53
+ */
54
+ export type NodeProps = Record<string, unknown> & {
55
+ _hidden?: boolean;
56
+ };
57
+
58
+ export interface DOMElement {
59
+ id: number;
60
+ type: ElementType;
61
+ props: NodeProps;
62
+ children: Array<DOMElement | TextNode>;
63
+ parent: DOMElement | null;
64
+ /** Called by the reconciler after each commit; wired up by `render()`. */
65
+ onRender: (() => void) | null;
66
+ /** Optional layout pass; we currently don't use this. */
67
+ onComputeLayout: (() => void) | null;
68
+ /** Internal dirty flag (currently informational only). */
69
+ _dirty: boolean;
70
+ }
71
+
72
+ export interface TextNode {
73
+ id: number;
74
+ type: '#text';
75
+ value: string;
76
+ parent: DOMElement | null;
77
+ /** Saved value while the node is hidden by Suspense. */
78
+ _hiddenValue?: string;
79
+ }
80
+
81
+ export type AnyNode = DOMElement | TextNode;
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Node creation
85
+ // ---------------------------------------------------------------------------
86
+
87
+ let nextNodeId = 1;
88
+
89
+ export function createNode(type: ElementType): DOMElement {
90
+ return {
91
+ id: nextNodeId++,
92
+ type,
93
+ props: {},
94
+ children: [],
95
+ parent: null,
96
+ onRender: null,
97
+ onComputeLayout: null,
98
+ _dirty: true,
99
+ };
100
+ }
101
+
102
+ export function createTextNode(text: string): TextNode {
103
+ return {
104
+ id: nextNodeId++,
105
+ type: '#text',
106
+ value: text,
107
+ parent: null,
108
+ };
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Tree manipulation
113
+ // ---------------------------------------------------------------------------
114
+
115
+ export function appendChildNode(parent: DOMElement, child: AnyNode): void {
116
+ if (child.parent) {
117
+ removeChildNode(child.parent, child);
118
+ }
119
+ child.parent = parent;
120
+ parent.children.push(child);
121
+ }
122
+
123
+ export function insertBeforeNode(
124
+ parent: DOMElement,
125
+ child: AnyNode,
126
+ beforeChild: AnyNode,
127
+ ): void {
128
+ if (child.parent) {
129
+ removeChildNode(child.parent, child);
130
+ }
131
+ child.parent = parent;
132
+ const idx = parent.children.indexOf(beforeChild);
133
+ if (idx >= 0) {
134
+ parent.children.splice(idx, 0, child);
135
+ } else {
136
+ parent.children.push(child);
137
+ }
138
+ }
139
+
140
+ export function removeChildNode(parent: DOMElement, child: AnyNode): void {
141
+ parent.children = parent.children.filter((c) => c !== child);
142
+ child.parent = null;
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Property / attribute helpers
147
+ // ---------------------------------------------------------------------------
148
+
149
+ export function setAttribute(node: DOMElement, key: string, value: unknown): void {
150
+ if (value === undefined) {
151
+ delete node.props[key];
152
+ } else {
153
+ node.props[key] = value;
154
+ }
155
+ }
156
+
157
+ export function setTextNodeValue(node: TextNode, text: string): void {
158
+ node.value = text;
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Tree traversal helpers
163
+ // ---------------------------------------------------------------------------
164
+
165
+ export function getTextContent(node: AnyNode): string {
166
+ if (node.type === '#text') {
167
+ return node.value;
168
+ }
169
+ return node.children.map(getTextContent).join('');
170
+ }
171
+
172
+ export type Visitor = (node: AnyNode, depth: number) => void;
173
+
174
+ export function walkTree(root: AnyNode, visitor: Visitor, depth = 0): void {
175
+ visitor(root, depth);
176
+ if (root.type !== '#text') {
177
+ for (const child of root.children) {
178
+ walkTree(child, visitor, depth + 1);
179
+ }
180
+ }
181
+ }
182
+
183
+ export function findRoot(node: AnyNode): DOMElement {
184
+ let current: AnyNode = node;
185
+ while (current.parent) {
186
+ current = current.parent;
187
+ }
188
+ // The root is always a DOMElement (created via createNode).
189
+ return current as DOMElement;
190
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * pebble-output.ts — Poco output layer for react-pebble on Pebble Alloy.
3
+ *
4
+ * Walks the virtual DOM tree (pebble-dom) and issues draw calls against
5
+ * a `commodetto/Poco` renderer, which writes into the watch framebuffer.
6
+ *
7
+ * Key differences from a canvas-style API:
8
+ *
9
+ * - **No stateful color or font.** Every draw call takes the color (an int
10
+ * produced by `poco.makeColor(r, g, b)`) and font (a `new poco.Font(...)`
11
+ * object) as arguments. We maintain per-Poco caches so we resolve each
12
+ * named color / font exactly once.
13
+ * - **No native line, circle, or stroked rectangle.** Poco only has
14
+ * `fillRectangle`, `drawText`, and bitmap draws. Outlines (stroke) are
15
+ * emulated as four thin fillRectangles. Axis-aligned lines likewise.
16
+ * Circles and diagonal lines would need the `commodetto/outline` extension
17
+ * — they're currently stubbed (TODO for a later pass).
18
+ * - **Text alignment is manual.** `drawText(text, font, color, x, y)` only
19
+ * draws at a point. For center/right alignment we measure with
20
+ * `getTextWidth` and compute the origin ourselves.
21
+ */
22
+
23
+ import type Poco from 'commodetto/Poco';
24
+ import type { PocoColor, PocoFont } from 'commodetto/Poco';
25
+ import type { DOMElement, NodeProps } from './pebble-dom.js';
26
+ import { getTextContent } from './pebble-dom.js';
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Named palette — mapped to RGB, then resolved to PocoColor via a cache.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export interface RGB {
33
+ r: number;
34
+ g: number;
35
+ b: number;
36
+ }
37
+
38
+ export const COLOR_PALETTE: Readonly<Record<string, RGB>> = {
39
+ black: { r: 0, g: 0, b: 0 },
40
+ white: { r: 255, g: 255, b: 255 },
41
+ red: { r: 255, g: 0, b: 0 },
42
+ green: { r: 0, g: 255, b: 0 },
43
+ blue: { r: 0, g: 0, b: 255 },
44
+ yellow: { r: 255, g: 255, b: 0 },
45
+ orange: { r: 255, g: 128, b: 0 },
46
+ cyan: { r: 0, g: 255, b: 255 },
47
+ magenta: { r: 255, g: 0, b: 255 },
48
+ clear: { r: 0, g: 0, b: 0 },
49
+ lightGray: { r: 192, g: 192, b: 192 },
50
+ darkGray: { r: 64, g: 64, b: 64 },
51
+ };
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Named font shortcuts — mapped to (family, size) pairs.
55
+ //
56
+ // The family names here need to match fonts available in the Moddable
57
+ // manifest. The Alloy scaffold ships "Bitham-Black" as a default — we use
58
+ // it for every logical font for now. Revisit once we add custom font
59
+ // resources to the library manifest.
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export interface FontSpec {
63
+ family: string;
64
+ size: number;
65
+ }
66
+
67
+ export const FONT_PALETTE: Readonly<Record<string, FontSpec>> = {
68
+ gothic14: { family: 'Bitham-Black', size: 14 },
69
+ gothic14Bold: { family: 'Bitham-Black', size: 14 },
70
+ gothic18: { family: 'Bitham-Black', size: 18 },
71
+ gothic18Bold: { family: 'Bitham-Black', size: 18 },
72
+ gothic24: { family: 'Bitham-Black', size: 24 },
73
+ gothic24Bold: { family: 'Bitham-Black', size: 24 },
74
+ gothic28: { family: 'Bitham-Black', size: 28 },
75
+ gothic28Bold: { family: 'Bitham-Black', size: 28 },
76
+ bitham30Black: { family: 'Bitham-Black', size: 30 },
77
+ bitham42Bold: { family: 'Bitham-Black', size: 42 },
78
+ bitham42Light: { family: 'Bitham-Black', size: 42 },
79
+ bitham34MediumNumbers: { family: 'Bitham-Black', size: 34 },
80
+ bitham42MediumNumbers: { family: 'Bitham-Black', size: 42 },
81
+ };
82
+
83
+ const DEFAULT_FONT_KEY = 'gothic18';
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Prop accessors — the DOM is loosely typed so we coerce here.
87
+ // ---------------------------------------------------------------------------
88
+
89
+ function num(p: NodeProps, key: string): number {
90
+ const v = p[key];
91
+ return typeof v === 'number' ? v : 0;
92
+ }
93
+
94
+ function str(p: NodeProps, key: string): string | undefined {
95
+ const v = p[key];
96
+ return typeof v === 'string' ? v : undefined;
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Renderer options
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export interface RenderOptions {
104
+ backgroundColor?: string;
105
+ /**
106
+ * Incremental update region. If provided, `poco.begin(x, y, w, h)` is used
107
+ * to clip drawing to just that region. Otherwise the full frame is redrawn.
108
+ */
109
+ dirty?: { x: number; y: number; w: number; h: number };
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // PocoRenderer — owns a Poco instance plus color/font caches
114
+ // ---------------------------------------------------------------------------
115
+
116
+ export class PocoRenderer {
117
+ readonly poco: Poco;
118
+ private readonly colorCache = new Map<string, PocoColor>();
119
+ private readonly fontCache = new Map<string, PocoFont>();
120
+
121
+ constructor(poco: Poco) {
122
+ this.poco = poco;
123
+ }
124
+
125
+ /**
126
+ * Render the full tree into a fresh frame.
127
+ */
128
+ render(rootNode: DOMElement, options: RenderOptions = {}): void {
129
+ const { poco } = this;
130
+ const dirty = options.dirty;
131
+
132
+ if (dirty) {
133
+ poco.begin(dirty.x, dirty.y, dirty.w, dirty.h);
134
+ } else {
135
+ poco.begin();
136
+ }
137
+
138
+ // Clear to background
139
+ const bg = this.getColor(options.backgroundColor ?? 'black');
140
+ poco.fillRectangle(bg, 0, 0, poco.width, poco.height);
141
+
142
+ // Walk the tree
143
+ this.renderChildren(rootNode, 0, 0);
144
+
145
+ poco.end();
146
+ }
147
+
148
+ /** Resolve a color name (or pass-through int) to a PocoColor. */
149
+ getColor(name: string | undefined): PocoColor {
150
+ const key = name ?? 'black';
151
+ const cached = this.colorCache.get(key);
152
+ if (cached !== undefined) return cached;
153
+
154
+ const rgb = COLOR_PALETTE[key] ?? COLOR_PALETTE.white!;
155
+ const color = this.poco.makeColor(rgb.r, rgb.g, rgb.b);
156
+ this.colorCache.set(key, color);
157
+ return color;
158
+ }
159
+
160
+ /** Resolve a font name to a PocoFont (cached). */
161
+ getFont(name: string | undefined): PocoFont {
162
+ const key = name ?? DEFAULT_FONT_KEY;
163
+ const cached = this.fontCache.get(key);
164
+ if (cached !== undefined) return cached;
165
+
166
+ const spec = FONT_PALETTE[key] ?? FONT_PALETTE[DEFAULT_FONT_KEY]!;
167
+ // `poco.Font` is a constructor hanging off the Poco instance.
168
+ const FontCtor = this.poco.Font;
169
+ const font = new FontCtor(spec.family, spec.size);
170
+ this.fontCache.set(key, font);
171
+ return font;
172
+ }
173
+
174
+ // -------------------------------------------------------------------------
175
+ // Private: node renderers
176
+ // -------------------------------------------------------------------------
177
+
178
+ private renderChildren(node: DOMElement, ox: number, oy: number): void {
179
+ for (const child of node.children) {
180
+ if (child.type === '#text') continue;
181
+ this.renderNode(child, ox, oy);
182
+ }
183
+ }
184
+
185
+ private renderNode(node: DOMElement, ox: number, oy: number): void {
186
+ const p = node.props;
187
+ if (p._hidden) return;
188
+
189
+ const x = num(p, 'x') + ox;
190
+ const y = num(p, 'y') + oy;
191
+
192
+ switch (node.type) {
193
+ case 'pbl-rect': {
194
+ const w = num(p, 'w') || num(p, 'width');
195
+ const h = num(p, 'h') || num(p, 'height');
196
+ const fill = str(p, 'fill');
197
+ const stroke = str(p, 'stroke');
198
+
199
+ if (fill) {
200
+ this.poco.fillRectangle(this.getColor(fill), x, y, w, h);
201
+ }
202
+ if (stroke) {
203
+ // Emulate outline with four thin fill rects.
204
+ const sw = num(p, 'strokeWidth') || 1;
205
+ const c = this.getColor(stroke);
206
+ this.poco.fillRectangle(c, x, y, w, sw); // top
207
+ this.poco.fillRectangle(c, x, y + h - sw, w, sw); // bottom
208
+ this.poco.fillRectangle(c, x, y, sw, h); // left
209
+ this.poco.fillRectangle(c, x + w - sw, y, sw, h); // right
210
+ }
211
+
212
+ this.renderChildren(node, x, y);
213
+ break;
214
+ }
215
+
216
+ case 'pbl-text': {
217
+ const text = getTextContent(node);
218
+ if (!text) break;
219
+
220
+ const boxW = num(p, 'w') || num(p, 'width') || this.poco.width - x;
221
+ const font = this.getFont(str(p, 'font'));
222
+ const color = this.getColor(str(p, 'color') ?? 'white');
223
+ const align = str(p, 'align') ?? 'left';
224
+
225
+ let tx = x;
226
+ if (align === 'center' || align === 'right') {
227
+ const tw = this.poco.getTextWidth(text, font);
228
+ if (align === 'center') {
229
+ tx = x + Math.floor((boxW - tw) / 2);
230
+ } else {
231
+ tx = x + boxW - tw;
232
+ }
233
+ }
234
+
235
+ this.poco.drawText(text, font, color, tx, y);
236
+ break;
237
+ }
238
+
239
+ case 'pbl-line': {
240
+ // Only axis-aligned lines are supported natively. Diagonals would
241
+ // need the commodetto/outline extension — TODO.
242
+ const x2 = num(p, 'x2') + ox;
243
+ const y2 = num(p, 'y2') + oy;
244
+ const c = this.getColor(str(p, 'color') ?? str(p, 'stroke') ?? 'white');
245
+ const sw = num(p, 'strokeWidth') || 1;
246
+
247
+ if (x === x2) {
248
+ // Vertical
249
+ const top = Math.min(y, y2);
250
+ const h = Math.abs(y2 - y) || 1;
251
+ this.poco.fillRectangle(c, x, top, sw, h);
252
+ } else if (y === y2) {
253
+ // Horizontal
254
+ const left = Math.min(x, x2);
255
+ const w = Math.abs(x2 - x) || 1;
256
+ this.poco.fillRectangle(c, left, y, w, sw);
257
+ }
258
+ // else: diagonal line — silently skipped for now
259
+ break;
260
+ }
261
+
262
+ case 'pbl-circle': {
263
+ // TODO: implement via commodetto/outline extension (blendOutline)
264
+ // or a Bresenham-style approximation. Stubbed for now.
265
+ break;
266
+ }
267
+
268
+ case 'pbl-image': {
269
+ const bitmap = p.bitmap;
270
+ if (bitmap) {
271
+ this.poco.drawBitmap(bitmap as never, x, y);
272
+ }
273
+ break;
274
+ }
275
+
276
+ case 'pbl-group': {
277
+ this.renderChildren(node, x, y);
278
+ break;
279
+ }
280
+
281
+ case 'pbl-statusbar':
282
+ case 'pbl-actionbar': {
283
+ // Alloy has no built-in status/action bar UI; these are no-ops for now.
284
+ // An app that wants a status bar should draw its own.
285
+ break;
286
+ }
287
+
288
+ case 'pbl-root': {
289
+ this.renderChildren(node, ox, oy);
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Compatibility shims — still useful for mock-mode tests that want to
298
+ // resolve a color name without constructing a Poco. These return *names*
299
+ // rather than native handles.
300
+ // ---------------------------------------------------------------------------
301
+
302
+ export function resolveColorName(color: string | undefined): string {
303
+ if (!color) return 'black';
304
+ return color in COLOR_PALETTE ? color : 'black';
305
+ }
306
+
307
+ export function resolveFontName(font: string | undefined): string {
308
+ if (!font) return DEFAULT_FONT_KEY;
309
+ return font in FONT_PALETTE ? font : DEFAULT_FONT_KEY;
310
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * pebble-reconciler.ts — Preact-backed reconciler for react-pebble.
3
+ *
4
+ * Replaces the old react-reconciler host config. We don't need a custom
5
+ * reconciler at all with Preact — Preact's `render(vnode, parentDom)` does
6
+ * the diffing, and we provide a DOM-shaped container via `pebble-dom-shim`
7
+ * so Preact writes into our pebble-dom tree instead of a real DOM.
8
+ *
9
+ * The public surface (for pebble-render.ts) is:
10
+ * - createReconcilerContainer() → a pair of root shim + pebble-dom root
11
+ * - updateContainer(vnode, container) → runs preact.render()
12
+ * - unmountContainer(container) → runs preact.render(null, ...)
13
+ */
14
+
15
+ import { render as preactRender } from 'preact';
16
+ import type { ComponentChild } from 'preact';
17
+ import type { DOMElement } from './pebble-dom.js';
18
+ import { createShimRoot, shimDocument } from './pebble-dom-shim.js';
19
+ import type { ShimElement } from './pebble-dom-shim.js';
20
+
21
+ // Preact's render() references `document` internally. In non-browser
22
+ // environments (Node mock mode, Alloy XS) we shim it with our pebble-dom
23
+ // adapter so Preact doesn't crash.
24
+ if (typeof document === 'undefined') {
25
+ (globalThis as unknown as { document: unknown }).document = shimDocument;
26
+ }
27
+
28
+ export interface PebbleContainer {
29
+ shimRoot: ShimElement;
30
+ pblRoot: DOMElement;
31
+ }
32
+
33
+ export function createContainer(): PebbleContainer {
34
+ const shimRoot = createShimRoot();
35
+ return {
36
+ shimRoot,
37
+ pblRoot: shimRoot._pbl,
38
+ };
39
+ }
40
+
41
+ export function updateContainer(vnode: ComponentChild, container: PebbleContainer): void {
42
+ preactRender(vnode, container.shimRoot as unknown as Element);
43
+ }
44
+
45
+ export function unmountContainer(container: PebbleContainer): void {
46
+ preactRender(null, container.shimRoot as unknown as Element);
47
+ }
48
+
49
+ // For backwards-compat with the old default export pattern.
50
+ export default {
51
+ createContainer,
52
+ updateContainer,
53
+ unmountContainer,
54
+ };