diff --git a/.gitignore b/.gitignore index bdf488e..69ede03 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ node_modules package-lock.json yarn.lock .vscode +.rpt2_cache +dist diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..16b05da --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + clearMocks: true, + coveragePathIgnorePatterns: ['/node_modules/', '/test/'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'jsx'], + testMatch: [ + '**/__tests__/**/*.(js|ts)?(x)', + '**/?(*.)+(spec|test).(js|ts)?(x)' + ], + transform: { '.(ts|tsx|js|jsx)': 'ts-jest' } +}; diff --git a/jest.d.ts b/jest.d.ts new file mode 100644 index 0000000..8058989 --- /dev/null +++ b/jest.d.ts @@ -0,0 +1,5 @@ +declare namespace jest { + interface Matchers { + toMatchDOM(modes: any): CustomMatcher; + } +} diff --git a/package.json b/package.json index e6f871e..5be5f33 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,48 @@ { "name": "superfine", - "description": "Minimal view layer for creating declarative web user interfaces.", "version": "6.0.1", - "main": "dist/superfine.js", - "module": "src/index.js", - "license": "MIT", - "repository": "jorgebucaran/superfine", - "types": "superfine.d.ts", - "files": [ - "src", - "dist", - "superfine.d.ts" - ], - "author": "Jorge Bucaran", + "description": "Minimal view layer for creating declarative web user interfaces.", "keywords": [ - "superfine", "frontend", - "virtual dom", - "vdom" + "superfine", + "vdom", + "virtual dom" + ], + "repository": "jorgebucaran/superfine", + "license": "MIT", + "author": "Jorge Bucaran", + "files": [ + "dist" ], + "main": "dist/superfine.js", + "module": "dist/superfine.mjs", + "types": "dist/superfine.d.ts", "scripts": { - "test": "jest --coverage --no-cache", - "build": "npm run bundle && npm run minify", - "bundle": "rollup -i src/index.js -o dist/superfine.js -m -f umd -n superfine", - "minify": "uglifyjs dist/superfine.js -o dist/superfine.js -mc pure_funcs=['Object.defineProperty'] --source-map includeSources,url=superfine.js.map", - "prepare": "npm run build", - "release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" + "prebuild": "del dist", + "build": "rollup -c", + "postbuild": "npm run test", + "prepub": "npm run setup", + "pub": "npm version patch", + "postpub": "npm publish", + "presetup": "git clean -fdX", + "setup": "npm install", + "postsetup": "npm run build", + "start": "rollup -wc", + "test": "jest --coverage --no-cache" }, - "babel": { - "presets": "env" + "prettier": { + "printWidth": 80, + "semi": true, + "singleQuote": true }, "devDependencies": { - "babel-env": "2.4.1", - "jest": "^23.3.0", - "rollup": "^0.62.0", - "uglify-js": "^3.4.3" + "@types/jest": "^23.3.10", + "del-cli": "1.1.0", + "jest": "^23.6.0", + "rollup": "^0.67.4", + "rollup-plugin-terser": "3.0.0", + "rollup-plugin-typescript2": "0.18.0", + "ts-jest": "^23.10.5", + "typescript": "^3.2.1" } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..7fdb94c --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,23 @@ +const typescript = require('rollup-plugin-typescript2'); +const terser = require('rollup-plugin-terser').terser; + +export default [ + { + input: 'src/superfine.ts', + output: { format: 'umd', file: 'dist/superfine.js', name: 'superfine' }, + plugins: [ + typescript({ + tsconfigOverride: { + compilerOptions: { target: 'es5' }, + include: ['src'] + } + }), + terser() + ] + }, + { + input: 'src/superfine.ts', + output: { format: 'esm', file: 'dist/superfine.mjs' }, + plugins: [typescript({ tsconfigOverride: { include: ['src'] } })] + } +]; diff --git a/src/superfine.ts b/src/superfine.ts index 228a15e..fe6492b 100644 --- a/src/superfine.ts +++ b/src/superfine.ts @@ -1,111 +1,111 @@ -var DEFAULT = 0 -var RECYCLED_NODE = 1 -var TEXT_NODE = 2 +var DEFAULT = 0; +var RECYCLED_NODE = 1; +var TEXT_NODE = 2; -var XLINK_NS = "http://www.w3.org/1999/xlink" -var SVG_NS = "http://www.w3.org/2000/svg" +var XLINK_NS = 'http://www.w3.org/1999/xlink'; +var SVG_NS = 'http://www.w3.org/2000/svg'; -var EMPTY_OBJECT = {} -var EMPTY_ARRAY = [] +var EMPTY_OBJECT = {}; +var EMPTY_ARRAY = []; -var map = EMPTY_ARRAY.map -var isArray = Array.isArray +var map = EMPTY_ARRAY.map; +var isArray = Array.isArray; var merge = function(a, b) { - var target = {} + var target = {}; - for (var i in a) target[i] = a[i] - for (var i in b) target[i] = b[i] + for (var i in a) target[i] = a[i]; + for (var i in b) target[i] = b[i]; - return target -} + return target; +}; var eventProxy = function(event) { - return event.currentTarget.events[event.type](event) -} + return event.currentTarget.events[event.type](event); +}; var updateProperty = function(element, name, lastValue, nextValue, isSvg) { - if (name === "key") { - } else if (name === "style") { + if (name === 'key') { + } else if (name === 'style') { for (var i in merge(lastValue, nextValue)) { - var style = nextValue == null || nextValue[i] == null ? "" : nextValue[i] - if (i[0] === "-") { - element[name].setProperty(i, style) + var style = nextValue == null || nextValue[i] == null ? '' : nextValue[i]; + if (i[0] === '-') { + element[name].setProperty(i, style); } else { - element[name][i] = style + element[name][i] = style; } } } else { - if (name[0] === "o" && name[1] === "n") { - if (!element.events) element.events = {} + if (name[0] === 'o' && name[1] === 'n') { + if (!element.events) element.events = {}; - element.events[(name = name.slice(2))] = nextValue + element.events[(name = name.slice(2))] = nextValue; if (nextValue == null) { - element.removeEventListener(name, eventProxy) + element.removeEventListener(name, eventProxy); } else if (lastValue == null) { - element.addEventListener(name, eventProxy) + element.addEventListener(name, eventProxy); } } else { - var nullOrFalse = nextValue == null || nextValue === false + var nullOrFalse = nextValue == null || nextValue === false; if ( name in element && - name !== "list" && - name !== "draggable" && - name !== "spellcheck" && - name !== "translate" && + name !== 'list' && + name !== 'draggable' && + name !== 'spellcheck' && + name !== 'translate' && !isSvg ) { - element[name] = nextValue == null ? "" : nextValue + element[name] = nextValue == null ? '' : nextValue; if (nullOrFalse) { - element.removeAttribute(name) + element.removeAttribute(name); } } else { - var ns = isSvg && name !== (name = name.replace(/^xlink:?/, "")) + var ns = isSvg && name !== (name = name.replace(/^xlink:?/, '')); if (ns) { if (nullOrFalse) { - element.removeAttributeNS(XLINK_NS, name) + element.removeAttributeNS(XLINK_NS, name); } else { - element.setAttributeNS(XLINK_NS, name, nextValue) + element.setAttributeNS(XLINK_NS, name, nextValue); } } else { if (nullOrFalse) { - element.removeAttribute(name) + element.removeAttribute(name); } else { - element.setAttribute(name, nextValue) + element.setAttribute(name, nextValue); } } } } } -} +}; var createElement = function(node, lifecycle, isSvg) { var element = node.type === TEXT_NODE ? document.createTextNode(node.name) - : (isSvg = isSvg || node.name === "svg") - ? document.createElementNS(SVG_NS, node.name) - : document.createElement(node.name) + : (isSvg = isSvg || node.name === 'svg') + ? document.createElementNS(SVG_NS, node.name) + : document.createElement(node.name); - var props = node.props + var props = node.props; if (props.oncreate) { lifecycle.push(function() { - props.oncreate(element) - }) + props.oncreate(element); + }); } for (var i = 0, length = node.children.length; i < length; i++) { - element.appendChild(createElement(node.children[i], lifecycle, isSvg)) + element.appendChild(createElement(node.children[i], lifecycle, isSvg)); } for (var name in props) { - updateProperty(element, name, null, props[name], isSvg) + updateProperty(element, name, null, props[name], isSvg); } - return (node.element = element) -} + return (node.element = element); +}; var updateElement = function( element, @@ -117,65 +117,65 @@ var updateElement = function( ) { for (var name in merge(lastProps, nextProps)) { if ( - (name === "value" || name === "checked" + (name === 'value' || name === 'checked' ? element[name] : lastProps[name]) !== nextProps[name] ) { - updateProperty(element, name, lastProps[name], nextProps[name], isSvg) + updateProperty(element, name, lastProps[name], nextProps[name], isSvg); } } - var cb = isRecycled ? nextProps.oncreate : nextProps.onupdate + var cb = isRecycled ? nextProps.oncreate : nextProps.onupdate; if (cb != null) { lifecycle.push(function() { - cb(element, lastProps) - }) + cb(element, lastProps); + }); } -} +}; var removeChildren = function(node) { for (var i = 0, length = node.children.length; i < length; i++) { - removeChildren(node.children[i]) + removeChildren(node.children[i]); } - var cb = node.props.ondestroy + var cb = node.props.ondestroy; if (cb != null) { - cb(node.element) + cb(node.element); } - return node.element -} + return node.element; +}; var removeElement = function(parent, node) { var remove = function() { - parent.removeChild(removeChildren(node)) - } + parent.removeChild(removeChildren(node)); + }; - var cb = node.props && node.props.onremove + var cb = node.props && node.props.onremove; if (cb != null) { - cb(node.element, remove) + cb(node.element, remove); } else { - remove() + remove(); } -} +}; var getKey = function(node) { - return node == null ? null : node.key -} + return node == null ? null : node.key; +}; var createKeyMap = function(children, start, end) { - var out = {} - var key - var node + var out = {}; + var key; + var node; for (; start <= end; start++) { if ((key = (node = children[start]).key) != null) { - out[key] = node + out[key] = node; } } - return out -} + return out; +}; var patchElement = function( parent, @@ -183,7 +183,7 @@ var patchElement = function( lastNode, nextNode, lifecycle, - isSvg + isSvg? ) { if (nextNode === lastNode) { } else if ( @@ -192,45 +192,45 @@ var patchElement = function( nextNode.type === TEXT_NODE ) { if (lastNode.name !== nextNode.name) { - element.nodeValue = nextNode.name + element.nodeValue = nextNode.name; } } else if (lastNode == null || lastNode.name !== nextNode.name) { var newElement = parent.insertBefore( createElement(nextNode, lifecycle, isSvg), element - ) + ); - if (lastNode != null) removeElement(parent, lastNode) + if (lastNode != null) removeElement(parent, lastNode); - element = newElement + element = newElement; } else { updateElement( element, lastNode.props, nextNode.props, lifecycle, - (isSvg = isSvg || nextNode.name === "svg"), + (isSvg = isSvg || nextNode.name === 'svg'), lastNode.type === RECYCLED_NODE - ) + ); - var savedNode - var childNode + var savedNode; + var childNode; - var lastKey - var lastChildren = lastNode.children - var lastChStart = 0 - var lastChEnd = lastChildren.length - 1 + var lastKey; + var lastChildren = lastNode.children; + var lastChStart = 0; + var lastChEnd = lastChildren.length - 1; - var nextKey - var nextChildren = nextNode.children - var nextChStart = 0 - var nextChEnd = nextChildren.length - 1 + var nextKey; + var nextChildren = nextNode.children; + var nextChStart = 0; + var nextChEnd = nextChildren.length - 1; while (nextChStart <= nextChEnd && lastChStart <= lastChEnd) { - lastKey = getKey(lastChildren[lastChStart]) - nextKey = getKey(nextChildren[nextChStart]) + lastKey = getKey(lastChildren[lastChStart]); + nextKey = getKey(nextChildren[nextChStart]); - if (lastKey == null || lastKey !== nextKey) break + if (lastKey == null || lastKey !== nextKey) break; patchElement( element, @@ -239,17 +239,17 @@ var patchElement = function( nextChildren[nextChStart], lifecycle, isSvg - ) + ); - lastChStart++ - nextChStart++ + lastChStart++; + nextChStart++; } while (nextChStart <= nextChEnd && lastChStart <= lastChEnd) { - lastKey = getKey(lastChildren[lastChEnd]) - nextKey = getKey(nextChildren[nextChEnd]) + lastKey = getKey(lastChildren[lastChEnd]); + nextKey = getKey(nextChildren[nextChEnd]); - if (lastKey == null || lastKey !== nextKey) break + if (lastKey == null || lastKey !== nextKey) break; patchElement( element, @@ -258,10 +258,10 @@ var patchElement = function( nextChildren[nextChEnd], lifecycle, isSvg - ) + ); - lastChEnd-- - nextChEnd-- + lastChEnd--; + nextChEnd--; } if (lastChStart > lastChEnd) { @@ -269,29 +269,29 @@ var patchElement = function( element.insertBefore( createElement(nextChildren[nextChStart++], lifecycle, isSvg), (childNode = lastChildren[lastChStart]) && childNode.element - ) + ); } } else if (nextChStart > nextChEnd) { while (lastChStart <= lastChEnd) { - removeElement(element, lastChildren[lastChStart++]) + removeElement(element, lastChildren[lastChStart++]); } } else { - var lastKeyed = createKeyMap(lastChildren, lastChStart, lastChEnd) - var nextKeyed = {} + var lastKeyed = createKeyMap(lastChildren, lastChStart, lastChEnd); + var nextKeyed = {}; while (nextChStart <= nextChEnd) { - lastKey = getKey((childNode = lastChildren[lastChStart])) - nextKey = getKey(nextChildren[nextChStart]) + lastKey = getKey((childNode = lastChildren[lastChStart])); + nextKey = getKey(nextChildren[nextChStart]); if ( nextKeyed[lastKey] || (nextKey != null && nextKey === getKey(lastChildren[lastChStart + 1])) ) { if (lastKey == null) { - removeElement(element, childNode) + removeElement(element, childNode); } - lastChStart++ - continue + lastChStart++; + continue; } if (nextKey == null || lastNode.type === RECYCLED_NODE) { @@ -303,10 +303,10 @@ var patchElement = function( nextChildren[nextChStart], lifecycle, isSvg - ) - nextChStart++ + ); + nextChStart++; } - lastChStart++ + lastChStart++; } else { if (lastKey === nextKey) { patchElement( @@ -316,9 +316,9 @@ var patchElement = function( nextChildren[nextChStart], lifecycle, isSvg - ) - nextKeyed[nextKey] = true - lastChStart++ + ); + nextKeyed[nextKey] = true; + lastChStart++; } else { if ((savedNode = lastKeyed[nextKey]) != null) { patchElement( @@ -331,8 +331,8 @@ var patchElement = function( nextChildren[nextChStart], lifecycle, isSvg - ) - nextKeyed[nextKey] = true + ); + nextKeyed[nextKey] = true; } else { patchElement( element, @@ -341,29 +341,29 @@ var patchElement = function( nextChildren[nextChStart], lifecycle, isSvg - ) + ); } } - nextChStart++ + nextChStart++; } } while (lastChStart <= lastChEnd) { if (getKey((childNode = lastChildren[lastChStart++])) == null) { - removeElement(element, childNode) + removeElement(element, childNode); } } for (var key in lastKeyed) { if (nextKeyed[key] == null) { - removeElement(element, lastKeyed[key]) + removeElement(element, lastKeyed[key]); } } } } - return (nextNode.element = element) -} + return (nextNode.element = element); +}; var createVNode = function(name, props, children, element, key, type) { return { @@ -373,18 +373,18 @@ var createVNode = function(name, props, children, element, key, type) { element: element, key: key, type: type - } -} + }; +}; -var createTextVNode = function(text, element) { - return createVNode(text, EMPTY_OBJECT, EMPTY_ARRAY, element, null, TEXT_NODE) -} +var createTextVNode = function(text, element?) { + return createVNode(text, EMPTY_OBJECT, EMPTY_ARRAY, element, null, TEXT_NODE); +}; var recycleChild = function(element) { return element.nodeType === 3 // Node.TEXT_NODE ? createTextVNode(element.nodeValue, element) - : recycleElement(element) -} + : recycleElement(element); +}; var recycleElement = function(element) { return createVNode( @@ -394,50 +394,47 @@ var recycleElement = function(element) { element, null, RECYCLED_NODE - ) -} + ); +}; export var recycle = function(container) { - return recycleElement(container.children[0]) -} + return recycleElement(container.children[0]); +}; export var patch = function(lastNode, nextNode, container) { - var lifecycle = [] - - patchElement(container, container.children[0], lastNode, nextNode, lifecycle) + var lifecycle = []; - while (lifecycle.length > 0) lifecycle.pop()() + patchElement(container, container.children[0], lastNode, nextNode, lifecycle); - return nextNode -} + while (lifecycle.length > 0) lifecycle.pop()(); -export var h = function(name, props) { - var node - var rest = [] - var children = [] - var length = arguments.length + return nextNode; +}; - while (length-- > 2) rest.push(arguments[length]) +export var h = function(name, props?, ...rest) { + var node; + var children = []; + var length = arguments.length; if ((props = props == null ? {} : props).children != null) { if (rest.length <= 0) { - rest.push(props.children) + rest.push(props.children); } - delete props.children + delete props.children; } while (rest.length > 0) { if (isArray((node = rest.pop()))) { for (length = node.length; length-- > 0; ) { - rest.push(node[length]) + rest.push(node[length]); } } else if (node === false || node === true || node == null) { } else { - children.push(typeof node === "object" ? node : createTextVNode(node)) + children.push(typeof node === 'object' ? node : createTextVNode(node)); } } - return typeof name === "function" + return typeof name === 'function' ? name(props, (props.children = children)) - : createVNode(name, props, children, null, props.key, DEFAULT) -} + : createVNode(name, props, children, null, props.key, DEFAULT); +}; diff --git a/superfine.d.ts b/superfine.d.ts deleted file mode 100644 index d1a7c46..0000000 --- a/superfine.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -export as namespace superfine - -export type Children = VNode | string | number | null - -/** - * The virtual DOM representation of an Element. - */ -export interface VNode { - name: string, - props: Props, - children: Array, - element: Element | null, - key: string | null, - type: number -} - -/** - * Create a new virtual DOM node. A virtual DOM is a description of what a DOM should look like using a tree of virtual nodes. - * @param name The name of an Element or a function that returns a virtual DOM node. - * @param props HTML props, SVG props, DOM events, Lifecycle Events, and Keys. - * @param children The element's child nodes. - */ -export function h( - name: string, - props?: Props | null, - ...children: Array -): VNode - -/** - * Render a virtual DOM node into a DOM element container. - * - * @param {VNode} oldNode The last virtual DOM node. - * @param {VNode} nextNode The next virtual DOM node. - * @param {Element?} container A DOM element where the new virtual DOM will be rendered. - * @returns {VNode} Returns nextNode. - **/ -export function patch( - lastNode: VNode, - nextNode: VNode, - container: Element -): VNode - -declare global { - namespace JSX { - interface Element extends VNode {} - interface IntrinsicElements { - [elemName: string]: any - } - } -} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9390617 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationDir": "dist", + "lib": ["dom", "es2015"], + "module": "es2015", + "target": "es2015" + } +}