UNPKG

ink

Version:
293 lines 10.4 kB
import process from 'node:process'; import createReconciler from 'react-reconciler'; import { DefaultEventPriority, NoEventPriority, } from 'react-reconciler/constants.js'; import * as Scheduler from 'scheduler'; import Yoga from 'yoga-layout'; import { createContext, version as reactVersion } from 'react'; import { createTextNode, appendChildNode, insertBeforeNode, removeChildNode, emitLayoutListeners, setStyle, setTextNodeValue, createNode, setAttribute, } from './dom.js'; import applyStyles from './styles.js'; // We need to conditionally perform devtools connection to avoid // accidentally breaking other third-party code. // See https://github.com/vadimdemedes/ink/issues/384 // See https://github.com/vadimdemedes/ink/issues/648 if (process.env['DEV'] === 'true') { // Intentionally no warning when the package is missing. // DEV may be set for other reasons; devtools is opt-in via installing the package. let isDevtoolsInstalled = false; try { import.meta.resolve('react-devtools-core'); isDevtoolsInstalled = true; } catch { } if (isDevtoolsInstalled) { await import('./devtools.js'); } } const diff = (before, after) => { if (before === after) { return; } if (!before) { return after; } const changed = {}; let isChanged = false; for (const key of Object.keys(before)) { const isDeleted = after ? !Object.hasOwn(after, key) : true; if (isDeleted) { changed[key] = undefined; isChanged = true; } } if (after) { for (const key of Object.keys(after)) { if (after[key] !== before[key]) { changed[key] = after[key]; isChanged = true; } } } return isChanged ? changed : undefined; }; const cleanupYogaNode = (node) => { node?.unsetMeasureFunc(); node?.freeRecursive(); }; let currentUpdatePriority = NoEventPriority; let currentRootNode; async function loadPackageJson() { const fs = await import('node:fs'); const content = fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'); const parsedContent = JSON.parse(content); return { name: parsedContent?.name, version: parsedContent?.version, }; } let packageInfo = { name: 'ink', version: reactVersion, }; if (process.env['DEV'] === 'true') { try { const loaded = await loadPackageJson(); packageInfo = { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing name: loaded.name || packageInfo.name, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing version: loaded.version || packageInfo.version, }; } catch (error) { console.warn('Failed to load package.json in development mode. Falling back to default renderer metadata.', error); } } export default createReconciler({ getRootHostContext: () => ({ isInsideText: false, }), prepareForCommit: () => null, preparePortalMount: () => null, clearContainer: () => false, resetAfterCommit(rootNode) { if (typeof rootNode.onComputeLayout === 'function') { rootNode.onComputeLayout(); } emitLayoutListeners(rootNode); // Since renders are throttled at the instance level and <Static> component children // are rendered only once and then get deleted, we need an escape hatch to // trigger an immediate render to ensure <Static> children are written to output before they get erased if (rootNode.isStaticDirty) { rootNode.isStaticDirty = false; if (typeof rootNode.onImmediateRender === 'function') { rootNode.onImmediateRender(); } return; } if (typeof rootNode.onRender === 'function') { rootNode.onRender(); } }, getChildHostContext(parentHostContext, type) { const previousIsInsideText = parentHostContext.isInsideText; const isInsideText = type === 'ink-text' || type === 'ink-virtual-text'; if (previousIsInsideText === isInsideText) { return parentHostContext; } return { isInsideText }; }, shouldSetTextContent: () => false, createInstance(originalType, newProps, rootNode, hostContext) { if (hostContext.isInsideText && originalType === 'ink-box') { throw new Error(`<Box> can’t be nested inside <Text> component`); } const type = originalType === 'ink-text' && hostContext.isInsideText ? 'ink-virtual-text' : originalType; const node = createNode(type); for (const [key, value] of Object.entries(newProps)) { if (key === 'children') { continue; } if (key === 'style') { setStyle(node, value); if (node.yogaNode) { applyStyles(node.yogaNode, value); } continue; } if (key === 'internal_transform') { node.internal_transform = value; continue; } if (key === 'internal_static') { currentRootNode = rootNode; node.internal_static = true; rootNode.isStaticDirty = true; // Save reference to <Static> node to skip traversal of entire // node tree to find it rootNode.staticNode = node; continue; } setAttribute(node, key, value); } return node; }, createTextInstance(text, _root, hostContext) { if (!hostContext.isInsideText) { throw new Error(`Text string "${text}" must be rendered inside <Text> component`); } return createTextNode(text); }, resetTextContent() { }, hideTextInstance(node) { setTextNodeValue(node, ''); }, unhideTextInstance(node, text) { setTextNodeValue(node, text); }, getPublicInstance: instance => instance, hideInstance(node) { node.yogaNode?.setDisplay(Yoga.DISPLAY_NONE); }, unhideInstance(node) { node.yogaNode?.setDisplay(Yoga.DISPLAY_FLEX); }, appendInitialChild: appendChildNode, appendChild: appendChildNode, insertBefore: insertBeforeNode, finalizeInitialChildren() { return false; }, isPrimaryRenderer: true, supportsMutation: true, supportsPersistence: false, supportsHydration: false, // Scheduler integration for concurrent mode supportsMicrotasks: true, scheduleMicrotask: queueMicrotask, // @ts-expect-error @types/react-reconciler is outdated and doesn't include scheduleCallback scheduleCallback: Scheduler.unstable_scheduleCallback, cancelCallback: Scheduler.unstable_cancelCallback, shouldYield: Scheduler.unstable_shouldYield, now: Scheduler.unstable_now, scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, noTimeout: -1, beforeActiveInstanceBlur() { }, afterActiveInstanceBlur() { }, detachDeletedInstance() { }, getInstanceFromNode: () => null, prepareScopeUpdate() { }, getInstanceFromScope: () => null, appendChildToContainer: appendChildNode, insertInContainerBefore: insertBeforeNode, removeChildFromContainer(node, removeNode) { removeChildNode(node, removeNode); cleanupYogaNode(removeNode.yogaNode); if (removeNode.internal_static && currentRootNode) { currentRootNode.staticNode = undefined; } }, commitUpdate(node, _type, oldProps, newProps) { if (currentRootNode && node.internal_static) { currentRootNode.isStaticDirty = true; } const props = diff(oldProps, newProps); const style = diff(oldProps['style'], newProps['style']); if (!props && !style) { return; } if (props) { for (const [key, value] of Object.entries(props)) { if (key === 'style') { setStyle(node, value); continue; } if (key === 'internal_transform') { node.internal_transform = value; continue; } if (key === 'internal_static') { node.internal_static = true; continue; } setAttribute(node, key, value); } } if (style && node.yogaNode) { applyStyles(node.yogaNode, style, newProps['style'] ?? {}); } }, commitTextUpdate(node, _oldText, newText) { setTextNodeValue(node, newText); }, removeChild(node, removeNode) { removeChildNode(node, removeNode); cleanupYogaNode(removeNode.yogaNode); if (removeNode.internal_static && currentRootNode) { currentRootNode.staticNode = undefined; } }, setCurrentUpdatePriority(newPriority) { currentUpdatePriority = newPriority; }, getCurrentUpdatePriority: () => currentUpdatePriority, resolveUpdatePriority() { if (currentUpdatePriority !== NoEventPriority) { return currentUpdatePriority; } return DefaultEventPriority; }, maySuspendCommit() { // Return true to enable Suspense resource preloading return true; }, // eslint-disable-next-line @typescript-eslint/naming-convention NotPendingTransition: undefined, // eslint-disable-next-line @typescript-eslint/naming-convention HostTransitionContext: createContext(null), resetFormInstance() { }, requestPostPaintCallback() { }, shouldAttemptEagerTransition() { return false; }, trackSchedulerEvent() { }, resolveEventType() { return null; }, resolveEventTimeStamp() { return -1.1; }, preloadInstance() { return true; }, startSuspendingCommit() { }, suspendInstance() { }, waitForCommitToBeReady() { return null; }, rendererPackageName: packageInfo.name, rendererVersion: packageInfo.version, }); //# sourceMappingURL=reconciler.js.map