paintcannon-react 0.0.1

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 (58) hide show
  1. package/README.md +213 -0
  2. package/dist/src/hooks/app-context.d.ts +8 -0
  3. package/dist/src/hooks/app-context.js +2 -0
  4. package/dist/src/hooks/index.d.ts +2 -0
  5. package/dist/src/hooks/index.js +2 -0
  6. package/dist/src/hooks/use-animation.d.ts +35 -0
  7. package/dist/src/hooks/use-animation.js +120 -0
  8. package/dist/src/hooks/use-app.d.ts +9 -0
  9. package/dist/src/hooks/use-app.js +9 -0
  10. package/dist/src/host-components/button/button.d.ts +8 -0
  11. package/dist/src/host-components/button/button.js +3 -0
  12. package/dist/src/host-components/button/reconcile-button.d.ts +6 -0
  13. package/dist/src/host-components/button/reconcile-button.js +14 -0
  14. package/dist/src/host-components/button.d.ts +8 -0
  15. package/dist/src/host-components/button.js +3 -0
  16. package/dist/src/host-components/div/div.d.ts +6 -0
  17. package/dist/src/host-components/div/div.js +3 -0
  18. package/dist/src/host-components/div/reconcile-div.d.ts +6 -0
  19. package/dist/src/host-components/div/reconcile-div.js +10 -0
  20. package/dist/src/host-components/div.d.ts +6 -0
  21. package/dist/src/host-components/div.js +3 -0
  22. package/dist/src/host-components/form/form.d.ts +6 -0
  23. package/dist/src/host-components/form/form.js +3 -0
  24. package/dist/src/host-components/form/reconcile-form.d.ts +6 -0
  25. package/dist/src/host-components/form/reconcile-form.js +10 -0
  26. package/dist/src/host-components/form.d.ts +6 -0
  27. package/dist/src/host-components/form.js +3 -0
  28. package/dist/src/host-components/index.d.ts +14 -0
  29. package/dist/src/host-components/index.js +14 -0
  30. package/dist/src/host-components/input/input.d.ts +8 -0
  31. package/dist/src/host-components/input/input.js +3 -0
  32. package/dist/src/host-components/input/reconcile-input.d.ts +6 -0
  33. package/dist/src/host-components/input/reconcile-input.js +14 -0
  34. package/dist/src/host-components/input.d.ts +8 -0
  35. package/dist/src/host-components/input.js +3 -0
  36. package/dist/src/host-components/mounted.d.ts +38 -0
  37. package/dist/src/host-components/mounted.js +17 -0
  38. package/dist/src/host-components/reconcile-shared.d.ts +8 -0
  39. package/dist/src/host-components/reconcile-shared.js +26 -0
  40. package/dist/src/host-components/shared.d.ts +40 -0
  41. package/dist/src/host-components/shared.js +18 -0
  42. package/dist/src/host-components/span/reconcile-span.d.ts +6 -0
  43. package/dist/src/host-components/span/reconcile-span.js +10 -0
  44. package/dist/src/host-components/span/span.d.ts +6 -0
  45. package/dist/src/host-components/span/span.js +3 -0
  46. package/dist/src/host-components/span.d.ts +6 -0
  47. package/dist/src/host-components/span.js +3 -0
  48. package/dist/src/host-components/textarea/reconcile-textarea.d.ts +6 -0
  49. package/dist/src/host-components/textarea/reconcile-textarea.js +12 -0
  50. package/dist/src/host-components/textarea/textarea.d.ts +8 -0
  51. package/dist/src/host-components/textarea/textarea.js +3 -0
  52. package/dist/src/host-components/textarea.d.ts +8 -0
  53. package/dist/src/host-components/textarea.js +3 -0
  54. package/dist/src/index.d.ts +21 -0
  55. package/dist/src/index.js +9 -0
  56. package/dist/src/reconciler.d.ts +17 -0
  57. package/dist/src/reconciler.js +403 -0
  58. package/package.json +39 -0
@@ -0,0 +1,403 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import React from "react";
5
+ import createReconciler from "react-reconciler";
6
+ import { DefaultEventPriority, NoEventPriority } from "react-reconciler/constants.js";
7
+ import * as Scheduler from "scheduler";
8
+ import { ELEMENT_EVENT_TYPES, PaintCannon } from "paintcannon";
9
+ import { AnimationContext, AnimationScheduler, AppContext } from "./hooks/index.js";
10
+ import * as hostComponents from "./host-components/index.js";
11
+ let currentUpdatePriority = NoEventPriority;
12
+ const sourceDirectory = path.dirname(fileURLToPath(import.meta.url));
13
+ const packageInfo = loadPackageInfo();
14
+ const eventListenerWrappers = new WeakMap();
15
+ const reconciler = createReconciler({
16
+ getRootHostContext: () => ({}),
17
+ getChildHostContext: () => ({}),
18
+ prepareForCommit(container) {
19
+ container.paintCannon.beginTransaction();
20
+ return null;
21
+ },
22
+ resetAfterCommit(container) {
23
+ container.paintCannon.commitTransaction();
24
+ container.paintCannon.render();
25
+ },
26
+ preparePortalMount: () => null,
27
+ clearContainer(container) {
28
+ for (const child of container.children) {
29
+ destroyHostNode(child);
30
+ }
31
+ container.children = [];
32
+ return false;
33
+ },
34
+ shouldSetTextContent: () => false,
35
+ createInstance(type, props, container) {
36
+ return createHostElement(container.paintCannon, type, props);
37
+ },
38
+ createTextInstance(text, container) {
39
+ return {
40
+ kind: "text",
41
+ text,
42
+ node: container.paintCannon.createTextNode(text),
43
+ };
44
+ },
45
+ appendInitialChild(parent, child) {
46
+ appendVirtualChild(parent, child);
47
+ appendPaintChild(parent.node, child.node);
48
+ },
49
+ appendChild(parent, child) {
50
+ appendVirtualChild(parent, child);
51
+ appendPaintChild(parent.node, child.node);
52
+ },
53
+ appendChildToContainer(container, child) {
54
+ appendVirtualChild(container, child);
55
+ container.root.appendChild(child.node);
56
+ },
57
+ insertBefore(parent, child, before) {
58
+ insertVirtualChild(parent, child, before);
59
+ insertPaintChild(parent.node, child.node, before.node);
60
+ },
61
+ insertInContainerBefore(container, child, before) {
62
+ insertVirtualChild(container, child, before);
63
+ container.root.insertBefore(child.node, before.node);
64
+ },
65
+ removeChild(parent, child) {
66
+ removeVirtualChild(parent, child);
67
+ destroyHostNode(child);
68
+ },
69
+ removeChildFromContainer(container, child) {
70
+ removeVirtualChild(container, child);
71
+ destroyHostNode(child);
72
+ },
73
+ finalizeInitialChildren: () => false,
74
+ resetTextContent: () => { },
75
+ getPublicInstance(instance) {
76
+ return instance.node;
77
+ },
78
+ commitUpdate(instance, _type, _oldProps, newProps) {
79
+ instance.props = applyHostElementProps(instance, newProps);
80
+ },
81
+ commitTextUpdate(instance, _oldText, newText) {
82
+ instance.text = newText;
83
+ instance.node.nodeValue = newText;
84
+ },
85
+ hideInstance(instance) {
86
+ instance.node.style.display = "none";
87
+ },
88
+ unhideInstance(instance) {
89
+ instance.node.style.display = defaultDisplay(instance.type);
90
+ },
91
+ hideTextInstance(instance) {
92
+ instance.node.nodeValue = "";
93
+ },
94
+ unhideTextInstance(instance, text) {
95
+ instance.node.nodeValue = text;
96
+ },
97
+ detachDeletedInstance: () => { },
98
+ beforeActiveInstanceBlur: () => { },
99
+ afterActiveInstanceBlur: () => { },
100
+ getInstanceFromNode: () => null,
101
+ prepareScopeUpdate: () => { },
102
+ getInstanceFromScope: () => null,
103
+ isPrimaryRenderer: false,
104
+ supportsMutation: true,
105
+ supportsPersistence: false,
106
+ supportsHydration: false,
107
+ supportsMicrotasks: true,
108
+ scheduleMicrotask: queueMicrotask,
109
+ // @ts-expect-error @types/react-reconciler omits scheduler integration hooks.
110
+ scheduleCallback: Scheduler.unstable_scheduleCallback,
111
+ cancelCallback: Scheduler.unstable_cancelCallback,
112
+ shouldYield: Scheduler.unstable_shouldYield,
113
+ now: Scheduler.unstable_now,
114
+ scheduleTimeout: setTimeout,
115
+ cancelTimeout: clearTimeout,
116
+ noTimeout: -1,
117
+ setCurrentUpdatePriority(priority) {
118
+ currentUpdatePriority = priority;
119
+ },
120
+ getCurrentUpdatePriority: () => currentUpdatePriority,
121
+ resolveUpdatePriority() {
122
+ return currentUpdatePriority !== NoEventPriority ? currentUpdatePriority : DefaultEventPriority;
123
+ },
124
+ maySuspendCommit: () => false,
125
+ NotPendingTransition: undefined,
126
+ HostTransitionContext: React.createContext(null),
127
+ resetFormInstance: () => { },
128
+ requestPostPaintCallback: () => { },
129
+ shouldAttemptEagerTransition: () => false,
130
+ trackSchedulerEvent: () => { },
131
+ resolveEventType: () => null,
132
+ resolveEventTimeStamp: () => -1.1,
133
+ preloadInstance: () => true,
134
+ startSuspendingCommit: () => { },
135
+ suspendInstance: () => { },
136
+ waitForCommitToBeReady: () => null,
137
+ rendererPackageName: packageInfo.name,
138
+ rendererVersion: packageInfo.version,
139
+ });
140
+ export function createRoot(options = {}) {
141
+ const paintCannon = options.paintCannon ?? new PaintCannon(options);
142
+ const container = options.container ?? paintCannon.createElement("div");
143
+ if (options.container === undefined) {
144
+ container.style.width = "100%";
145
+ container.style.height = "100%";
146
+ }
147
+ paintCannon.setRoot(container);
148
+ const rootContainer = {
149
+ paintCannon,
150
+ root: container,
151
+ children: [],
152
+ };
153
+ const animationScheduler = new AnimationScheduler(paintCannon);
154
+ const reactRoot = reconciler.createContainer(rootContainer, 0, null, false, null, "", reportReactError, reportReactError, reportReactError, () => { });
155
+ let exited = false;
156
+ let resolveExit;
157
+ let rejectExit;
158
+ const exitPromise = new Promise((resolve, reject) => {
159
+ resolveExit = resolve;
160
+ rejectExit = reject;
161
+ });
162
+ const settleExit = (errorOrResult) => {
163
+ if (errorOrResult instanceof Error) {
164
+ rejectExit(errorOrResult);
165
+ return;
166
+ }
167
+ resolveExit(errorOrResult);
168
+ };
169
+ const app = {
170
+ paintCannon,
171
+ exit(errorOrResult) {
172
+ if (exited) {
173
+ return;
174
+ }
175
+ exited = true;
176
+ reconciler.updateContainer(null, reactRoot, null, () => {
177
+ animationScheduler.stop();
178
+ paintCannon.stop();
179
+ settleExit(errorOrResult);
180
+ });
181
+ },
182
+ waitUntilExit() {
183
+ return exitPromise;
184
+ },
185
+ };
186
+ return {
187
+ paintCannon,
188
+ container,
189
+ render(element) {
190
+ if (exited) {
191
+ throw new Error("paintcannon-react root has exited");
192
+ }
193
+ reconciler.updateContainer(React.createElement(AppContext.Provider, { value: app }, React.createElement(AnimationContext.Provider, { value: animationScheduler }, element)), reactRoot, null, null);
194
+ },
195
+ unmount() {
196
+ app.exit();
197
+ },
198
+ exit(errorOrResult) {
199
+ app.exit(errorOrResult);
200
+ },
201
+ waitUntilExit() {
202
+ return app.waitUntilExit();
203
+ },
204
+ };
205
+ }
206
+ export function render(element, options = {}) {
207
+ const root = createRoot(options);
208
+ root.render(element);
209
+ return root;
210
+ }
211
+ function appendVirtualChild(parent, child) {
212
+ removeVirtualChild(parent, child);
213
+ parent.children.push(child);
214
+ }
215
+ function insertVirtualChild(parent, child, before) {
216
+ removeVirtualChild(parent, child);
217
+ const index = parent.children.indexOf(before);
218
+ if (index === -1) {
219
+ parent.children.push(child);
220
+ }
221
+ else {
222
+ parent.children.splice(index, 0, child);
223
+ }
224
+ }
225
+ function removeVirtualChild(parent, child) {
226
+ const index = parent.children.indexOf(child);
227
+ if (index !== -1) {
228
+ parent.children.splice(index, 1);
229
+ }
230
+ }
231
+ function createHostElement(paintCannon, type, props) {
232
+ if (type === hostComponents.div.type) {
233
+ return hostComponents.reconcileDiv.create(paintCannon, hostComponents.castHostProps(type, props), applyCommonProps);
234
+ }
235
+ if (type === hostComponents.span.type) {
236
+ return hostComponents.reconcileSpan.create(paintCannon, hostComponents.castHostProps(type, props), applyCommonProps);
237
+ }
238
+ if (type === hostComponents.form.type) {
239
+ return hostComponents.reconcileForm.create(paintCannon, hostComponents.castHostProps(type, props), applyCommonProps);
240
+ }
241
+ if (type === hostComponents.button.type) {
242
+ return hostComponents.reconcileButton.create(paintCannon, hostComponents.castHostProps(type, props), applyCommonProps);
243
+ }
244
+ if (type === hostComponents.input.type) {
245
+ return hostComponents.reconcileInput.create(paintCannon, hostComponents.castHostProps(type, props), applyCommonProps);
246
+ }
247
+ if (type === hostComponents.textarea.type) {
248
+ return hostComponents.reconcileTextarea.create(paintCannon, hostComponents.castHostProps(type, props), applyCommonProps);
249
+ }
250
+ const unknownType = type;
251
+ throw new Error(`Unknown host type: ${String(unknownType)}`);
252
+ }
253
+ function appendPaintChild(parent, child) {
254
+ if (!canHaveChildren(parent)) {
255
+ throw new Error("Input and Textarea cannot have children");
256
+ }
257
+ parent.appendChild(child);
258
+ }
259
+ function insertPaintChild(parent, child, before) {
260
+ if (!canHaveChildren(parent)) {
261
+ throw new Error("Input and Textarea cannot have children");
262
+ }
263
+ parent.insertBefore(child, before);
264
+ }
265
+ function destroyHostNode(host) {
266
+ host.node.destroy();
267
+ }
268
+ function applyHostElementProps(host, newProps) {
269
+ if (host.type === hostComponents.div.type) {
270
+ const props = hostComponents.castHostProps(host.type, newProps);
271
+ hostComponents.reconcileDiv.applyProps(host.node, host.props, props, applyCommonProps);
272
+ return props;
273
+ }
274
+ if (host.type === hostComponents.span.type) {
275
+ const props = hostComponents.castHostProps(host.type, newProps);
276
+ hostComponents.reconcileSpan.applyProps(host.node, host.props, props, applyCommonProps);
277
+ return props;
278
+ }
279
+ if (host.type === hostComponents.form.type) {
280
+ const props = hostComponents.castHostProps(host.type, newProps);
281
+ hostComponents.reconcileForm.applyProps(host.node, host.props, props, applyCommonProps);
282
+ return props;
283
+ }
284
+ if (host.type === hostComponents.button.type) {
285
+ const props = hostComponents.castHostProps(host.type, newProps);
286
+ hostComponents.reconcileButton.applyProps(host.node, host.props, props, applyCommonProps);
287
+ return props;
288
+ }
289
+ if (host.type === hostComponents.input.type) {
290
+ const props = hostComponents.castHostProps(host.type, newProps);
291
+ hostComponents.reconcileInput.applyProps(host.node, host.props, props, applyCommonProps);
292
+ return props;
293
+ }
294
+ if (host.type === hostComponents.textarea.type) {
295
+ const props = hostComponents.castHostProps(host.type, newProps);
296
+ hostComponents.reconcileTextarea.applyProps(host.node, host.props, props, applyCommonProps);
297
+ return props;
298
+ }
299
+ const unknownHost = host;
300
+ throw new Error(`Unknown host type: ${String(unknownHost)}`);
301
+ }
302
+ function applyCommonProps(node, oldProps, newProps) {
303
+ applyStyle(node, oldProps.style, newProps.style);
304
+ applyEvents(node, oldProps, newProps);
305
+ }
306
+ function applyStyle(node, oldStyle, newStyle) {
307
+ if (oldStyle === newStyle) {
308
+ return;
309
+ }
310
+ for (const [key, value] of Object.entries(oldStyle ?? {})) {
311
+ const styleKey = key;
312
+ const nextValue = newStyle?.[styleKey];
313
+ if (value !== undefined && value !== null && (nextValue === undefined || nextValue === null)) {
314
+ node.style.removeProperty(styleKey);
315
+ }
316
+ }
317
+ for (const [key, value] of Object.entries(newStyle ?? {})) {
318
+ const styleKey = key;
319
+ if (value !== undefined && value !== null && value !== oldStyle?.[styleKey]) {
320
+ node.style.setProperty(styleKey, value);
321
+ }
322
+ }
323
+ }
324
+ function applyEvents(node, oldProps, newProps) {
325
+ for (const [prop, eventType] of eventProps) {
326
+ const previous = listenerProp(oldProps, prop);
327
+ const next = listenerProp(newProps, prop);
328
+ if (previous === next) {
329
+ continue;
330
+ }
331
+ if (previous !== undefined) {
332
+ removeElementListener(node, eventType, previous);
333
+ }
334
+ if (next !== undefined) {
335
+ addElementListener(node, eventType, next);
336
+ }
337
+ }
338
+ }
339
+ function listenerProp(props, prop) {
340
+ const value = props[prop];
341
+ return typeof value === "function" ? value : undefined;
342
+ }
343
+ function canHaveChildren(node) {
344
+ return "appendChild" in node && "insertBefore" in node;
345
+ }
346
+ function addElementListener(node, eventType, listener) {
347
+ node.addEventListener(eventType, reactEventListener(listener));
348
+ }
349
+ function removeElementListener(node, eventType, listener) {
350
+ node.removeEventListener(eventType, reactEventListener(listener));
351
+ }
352
+ function reactEventListener(listener) {
353
+ let wrapped = eventListenerWrappers.get(listener);
354
+ if (wrapped === undefined) {
355
+ wrapped = (event) => {
356
+ reconciler.flushSyncFromReconciler(() => {
357
+ listener(event);
358
+ });
359
+ };
360
+ eventListenerWrappers.set(listener, wrapped);
361
+ }
362
+ return wrapped;
363
+ }
364
+ function defaultDisplay(type) {
365
+ return type === hostComponents.span.type ? "inline" : "block";
366
+ }
367
+ function reportReactError(error) {
368
+ if (error !== null && error !== undefined) {
369
+ console.error(error);
370
+ }
371
+ }
372
+ function loadPackageInfo() {
373
+ const packageJsonPath = findPackageJsonPath();
374
+ const packageJson = fs.readFileSync(packageJsonPath, "utf8");
375
+ const parsed = JSON.parse(packageJson);
376
+ if (parsed?.name !== "paintcannon-react" || typeof parsed.version !== "string") {
377
+ throw new Error(`Invalid package metadata in ${packageJsonPath}`);
378
+ }
379
+ return {
380
+ name: parsed.name,
381
+ version: parsed.version,
382
+ };
383
+ }
384
+ function findPackageJsonPath() {
385
+ let directory = sourceDirectory;
386
+ while (true) {
387
+ const packageJsonPath = path.join(directory, "package.json");
388
+ if (fs.existsSync(packageJsonPath)) {
389
+ const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
390
+ if (parsed?.name === "paintcannon-react") {
391
+ return packageJsonPath;
392
+ }
393
+ }
394
+ const parent = path.dirname(directory);
395
+ if (parent === directory) {
396
+ throw new Error("Could not find paintcannon-react package metadata");
397
+ }
398
+ directory = parent;
399
+ }
400
+ }
401
+ const eventProps = [
402
+ ...ELEMENT_EVENT_TYPES.map(eventType => [hostComponents.EVENT_PROP_NAMES[eventType], eventType]),
403
+ ];
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "paintcannon-react",
3
+ "version": "0.0.1",
4
+ "main": "dist/src/index.js",
5
+ "types": "dist/src/index.d.ts",
6
+ "type": "module",
7
+ "files": [
8
+ "README.md",
9
+ "dist/src"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "demo": "npm run build && node dist/examples/counter.js",
14
+ "demo:controlled": "npm run build && node dist/examples/controlled.js",
15
+ "demo:form": "npm run build && node dist/examples/form.js",
16
+ "demo:keyboard": "npm run build && node dist/examples/keyboard.js",
17
+ "demo:lifecycle": "npm run build && node dist/examples/lifecycle.js",
18
+ "demo:spinner": "npm run build && node dist/examples/spinner.js",
19
+ "demo:todo": "npm run build && node dist/examples/todo.js",
20
+ "test": "vitest run",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "dependencies": {
24
+ "react-reconciler": "^0.31.0",
25
+ "scheduler": "^0.25.0"
26
+ },
27
+ "peerDependencies": {
28
+ "paintcannon": "^0.0.1",
29
+ "react": "^19.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^25.9.1",
33
+ "@types/react": "^19.0.0",
34
+ "@types/react-reconciler": "^0.33.0",
35
+ "@types/scheduler": "^0.26.0",
36
+ "typescript": "^6.0.3",
37
+ "vitest": "^4.1.7"
38
+ }
39
+ }