Skip to content

Commit

Permalink
[jsdom-compat] Optionally allow use of external DOM
Browse files Browse the repository at this point in the history
  • Loading branch information
Shaheen Gandhi committed Jun 28, 2019
1 parent 4422edd commit 13adf40
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 162 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"cli-cursor": "^2.1.0",
"cli-truncate": "^1.1.0",
"is-ci": "^2.0.0",
"jsdom": "^15.1.1",
"lodash.throttle": "^4.1.1",
"log-update": "^3.0.0",
"prop-types": "^15.6.2",
Expand All @@ -72,6 +73,7 @@
"eslint-plugin-react": "^7.11.1",
"eslint-plugin-react-hooks": "^1.4.0",
"import-jsx": "^1.3.0",
"inspect-process": "^0.5.0",
"ms": "^2.1.1",
"node-pty": "^0.8.1",
"p-queue": "^3.0.0",
Expand All @@ -82,7 +84,8 @@
"xo": "^0.24.0"
},
"peerDependencies": {
"react": ">=16.8.0"
"react": ">=16.8.0",
"react-dom": "^16.8.6"
},
"babel": {
"plugins": [
Expand Down
23 changes: 14 additions & 9 deletions src/build-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import applyStyles from './apply-styles';
import measureText from './measure-text';

// Traverse the node tree, create Yoga nodes and assign styles to each Yoga node
const buildLayout = (node, options) => {
const buildLayout = (documentHelpers, node, options) => {
const {config, terminalWidth, skipStaticElements} = options;
const yogaNode = Yoga.Node.create(config);
node.yogaNode = yogaNode;
Expand All @@ -15,13 +15,15 @@ const buildLayout = (node, options) => {
// `terminalWidth` can be `undefined` if env isn't a TTY
yogaNode.setWidth(terminalWidth || 100);

if (node.childNodes.length > 0) {
const childNodes = node.childNodes.filter(childNode => {
const childNodes1 = documentHelpers.getChildNodes(node);

if (childNodes1.length > 0) {
const childNodes = childNodes1.filter(childNode => {
return skipStaticElements ? !childNode.unstable__static : true;
});

for (const [index, childNode] of Object.entries(childNodes)) {
const childYogaNode = buildLayout(childNode, options).yogaNode;
const childYogaNode = buildLayout(documentHelpers, childNode, options).yogaNode;
yogaNode.insertChild(childYogaNode, index);
}
}
Expand All @@ -33,21 +35,24 @@ const buildLayout = (node, options) => {
applyStyles(yogaNode, style);

// Nodes with only text have a child Yoga node dedicated for that text
if (node.textContent || node.nodeValue) {
const {width, height} = measureText(node.textContent || node.nodeValue);
const textContent = documentHelpers.getTextContent(node);
if (textContent || node.nodeValue) {
const {width, height} = measureText(textContent || node.nodeValue);
yogaNode.setWidth(style.width || width);
yogaNode.setHeight(style.height || height);

return node;
}

if (Array.isArray(node.childNodes) && node.childNodes.length > 0) {
const childNodes = node.childNodes.filter(childNode => {
const childNodes1 = documentHelpers.getChildNodes(node);

if (Array.isArray(childNodes1) && childNodes1.length > 0) {
const childNodes = childNodes1.filter(childNode => {
return skipStaticElements ? !childNode.unstable__static : true;
});

for (const [index, childNode] of Object.entries(childNodes)) {
const {yogaNode: childYogaNode} = buildLayout(childNode, options);
const {yogaNode: childYogaNode} = buildLayout(documentHelpers, childNode, options);
yogaNode.insertChild(childYogaNode, index);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AppContext from './AppContext';
import StdinContext from './StdinContext';
import StdoutContext from './StdoutContext';


// Root component for all Ink apps
// It renders stdin and stdout contexts, so that children can access them if needed
// It also handles Ctrl+C exiting and cursor visibility
Expand Down
103 changes: 94 additions & 9 deletions src/dom.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,64 @@
// Helper utilities implementing some common DOM methods to simplify reconciliation code
export const createNode = tagName => ({
const _documentCreateNode = (document, tagName) => {
return document.createElement(tagName);
};

const _documentAppendChildNode = (node, childNode) => {
if (childNode.parentNode) {
childNode.parentNode.removeChild(childNode);
}

node.append(childNode);
}; // Same as `appendChildNode`, but without removing child node from parent node

const _documentAppendStaticNode = (node, childNode) => {
node.append(childNode);
};

const _documentInsertBeforeNode = (node, newChildNode, beforeChildNode) => {
if (newChildNode.parentNode) {
newChildNode.parentNode.removeChild(newChildNode);
}

node.insertBefore(newChildNode, beforeChildNode);
};

const _documentRemoveChildNode = (node, removeNode) => {
node.removeChild(removeNode);
};

const _documentSetAttribute = (node, key, value) => {
node.setAttribute(key, value);
};

const _documentCreateTextNode = (document, text) => {
return document.createTextNode(text);
};

const _documentGetChildNodes = node => {
return [...node.childNodes];
};

const _documentGetTextContent = node => {
if (node.nodeType === 3) {
return node.data;
}

return null;
};

// Helper utilities implementing some common DOM methods to simplify reconciliation code
const _createNode = tagName => ({
nodeName: tagName.toUpperCase(),
style: {},
attributes: {},
childNodes: [],
parentNode: null
});

export const appendChildNode = (node, childNode) => {
const _appendChildNode = (node, childNode) => {
if (childNode.parentNode) {
removeChildNode(childNode.parentNode, childNode);
_removeChildNode(childNode.parentNode, childNode);
}

childNode.parentNode = node;
Expand All @@ -18,13 +67,13 @@ export const appendChildNode = (node, childNode) => {
};

// Same as `appendChildNode`, but without removing child node from parent node
export const appendStaticNode = (node, childNode) => {
const _appendStaticNode = (node, childNode) => {
node.childNodes.push(childNode);
};

export const insertBeforeNode = (node, newChildNode, beforeChildNode) => {
const _insertBeforeNode = (node, newChildNode, beforeChildNode) => {
if (newChildNode.parentNode) {
removeChildNode(newChildNode.parentNode, newChildNode);
_removeChildNode(newChildNode.parentNode, newChildNode);
}

newChildNode.parentNode = node;
Expand All @@ -38,7 +87,7 @@ export const insertBeforeNode = (node, newChildNode, beforeChildNode) => {
node.childNodes.push(newChildNode);
};

export const removeChildNode = (node, removeNode) => {
const _removeChildNode = (node, removeNode) => {
removeNode.parentNode = null;

const index = node.childNodes.indexOf(removeNode);
Expand All @@ -47,11 +96,47 @@ export const removeChildNode = (node, removeNode) => {
}
};

export const setAttribute = (node, key, value) => {
const _setAttribute = (node, key, value) => {
node.attributes[key] = value;
};

export const createTextNode = text => ({
const _createTextNode = text => ({
nodeName: '#text',
nodeValue: text
});

const _getChildNodes = node => {
return node.childNodes;
};

const _getTextContent = node => {
return node.textContent;
};

export const createDocumentHelpers = document => {
if (document) {
return Object.freeze({
createNode: tagName => _documentCreateNode(document, tagName),
appendChildNode: _documentAppendChildNode,
appendStaticNode: _documentAppendStaticNode,
insertBeforeNode: _documentInsertBeforeNode,
removeChildNode: _documentRemoveChildNode,
setAttribute: _documentSetAttribute,
createTextNode: text => _documentCreateTextNode(document, text),
getChildNodes: _documentGetChildNodes,
getTextContent: _documentGetTextContent
});
}

return Object.freeze({
createNode: _createNode,
appendChildNode: _appendChildNode,
appendStaticNode: _appendStaticNode,
insertBeforeNode: _insertBeforeNode,
removeChildNode: _removeChildNode,
setAttribute: _setAttribute,
createTextNode: _createTextNode,
getChildNodes: _getChildNodes,
getTextContent: _getTextContent
});
};
19 changes: 13 additions & 6 deletions src/instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import autoBind from 'auto-bind';
import logUpdate from 'log-update';
import isCI from 'is-ci';
import signalExit from 'signal-exit';
import reconciler from './reconciler';
import {createReconciler} from './reconciler';
import createRenderer from './renderer';
import {createNode} from './dom';
import {createDocumentHelpers} from './dom';
import instances from './instances';
import App from './components/App';

Expand All @@ -15,10 +15,12 @@ export default class Instance {
autoBind(this);

this.options = options;
this.documentHelpers = createDocumentHelpers(options.document);

this.rootNode = createNode('root');
this.rootNode = this.documentHelpers.createNode('root');
this.rootNode.onRender = this.onRender;
this.renderer = createRenderer({
documentHelpers: this.documentHelpers,
terminalWidth: options.stdout.columns
});

Expand All @@ -38,7 +40,12 @@ export default class Instance {
// so that it's rerendered every time, not just new static parts, like in non-debug mode
this.fullStaticOutput = '';

this.container = reconciler.createContainer(this.rootNode, false, false);
this.reconciler = createReconciler(this.documentHelpers);
this.container = this.reconciler.createContainer(this.rootNode, false, false);

if (options.document) {
options.document.body.append(this.rootNode);
}

this.exitPromise = new Promise((resolve, reject) => {
this.resolveExitPromise = resolve;
Expand Down Expand Up @@ -102,7 +109,7 @@ export default class Instance {
</App>
);

reconciler.updateContainer(tree, this.container);
this.reconciler.updateContainer(tree, this.container);
}

unmount(error) {
Expand All @@ -122,7 +129,7 @@ export default class Instance {
}

this.isUnmounted = true;
reconciler.updateContainer(null, this.container);
this.reconciler.updateContainer(null, this.container);
instances.delete(this.options.stdout);

if (error instanceof Error) {
Expand Down
Loading

0 comments on commit 13adf40

Please sign in to comment.