Skip to content

Commit

Permalink
refactor: add typedefs and make type-safe
Browse files Browse the repository at this point in the history
TODO: type decls for tags export....maybe
  • Loading branch information
dbushong committed Oct 19, 2019
1 parent f77df0a commit 403b571
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 40 deletions.
68 changes: 38 additions & 30 deletions lib/phy.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,18 @@

const preact = require('preact');

/**
* @typedef {preact.VNode} VNode
* @typedef {preact.ComponentType} ComponentType
* @typedef {preact.ComponentChildren} ComponentChildren
*/

// VNode props https://github.com/preactjs/preact/blob/master/src/index.d.ts#L14-L17
const VNODE_ATTRS = ['props', 'type', 'key', 'ref'];

// simplified from lodash
const objectToString = Object.toString();

/** @param {VNode | Readonly<Record<string, any>>} obj */
function isAttributes(obj) {
return (
'object' === typeof obj &&
Expand All @@ -47,50 +54,51 @@ function isAttributes(obj) {
);
}

const re = /^([a-zA-Z\d-]+)|([.#])([\w-]+)/g;

// possible arg combos (eliding createElement) (kids* = 0-or-more-kids):
// h(selector: Component|string, kids*: string|Element|Array[kid])
// h(selector: Component|string, attrs: object|null, kids*: string|Element|Array[kid])
function h(createElement, selector, attrs) {
const kids = Array.from(arguments).slice(3);

/**
* @param {typeof preact.createElement} createElement
* @param {string | ComponentType} selector
* @param {Readonly<Record<string, any>>} [attrs]
* @param {ComponentChildren[]} kids
*/
function h(createElement, selector, attrs, ...kids) {
if (attrs) {
if (!isAttributes(attrs)) {
kids.unshift(attrs);
kids.unshift(/** @type {ComponentChildren} */ (attrs));
attrs = {};
}
} else attrs = {};

if ('string' !== typeof selector) return createElement(selector, attrs, kids);
if ('string' !== typeof selector) {
return createElement(selector, attrs, ...kids);
}

let tag = 'div';
let classes;

const { class: klass, className, ...restAttrs } = attrs;
const attrClass = klass || className;
/** @type {Set<string>} */
const classes = new Set(attrClass ? attrClass.trim().split(/\s+/) : []);

// recast to remove readonly
/** @type {Record<string, any>} */
const attrsOut = restAttrs;

const re = /^([a-zA-Z\d-]+)|([.#])([\w-]+)/g;
let m;
while ((m = re.exec(selector))) {
if (m[1]) tag = m[1];
else if (m[2] === '#') attrs.id = m[3];
else {
if (!classes) {
classes = attrs.class || attrs.className;
classes = classes
? classes.split(/\s+/).reduce((o, c) => {
o[c] = true;
return o;
}, {})
: {};
}
classes[m[3]] = true;
}
}
if (classes) {
attrs.class = Object.keys(classes).join(' ');
delete attrs.className;
const [, explicitTag, sep, classOrId] = m;
if (explicitTag) tag = explicitTag;
else if (sep === '#') attrsOut.id = classOrId;
else classes.add(classOrId);
}

return createElement.apply(null, [tag, attrs].concat(kids));
if (classes.size > 0) attrsOut.class = [...classes].join(' ');

return createElement(tag, attrsOut, ...kids);
}

/** @type {import('./typedefs')} */
module.exports = exports = h.bind(null, preact.createElement);

exports.h = exports;
Expand Down
19 changes: 17 additions & 2 deletions lib/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,27 @@

'use strict';

/**
* @param {typeof import('../').h} h
* @param {string} tag
* @param {string} [selector]
* @param {Readonly<Record<string, any>>} [attrs]
* @param {import('preact').ComponentChildren[]} kids
*/
function hhTags(h, tag, selector, attrs, kids) {
if ('string' === typeof selector) {
if (!/^[.#]/.test(selector))
// e.g. span('oops')
if (!/^[.#]/.test(selector)) {
throw new Error(`${tag}(): string children must be passed in an array`);
}
// e.g. span('#photo', { alt: 'photo spot' }, ['photo here'])
// or span('#photo', ['photo here']) or span('#photo')
return h(`${tag}${selector}`, attrs, kids);
}
// no selector given, pass next two args shifted

// no selector given, pass remaining args shifted
// e.g. span({ alt: 'photo spot' }, ['photo here'])
// or span(['photo here'])
return h(tag, selector, attrs);
}

Expand All @@ -52,5 +66,6 @@ const TAG_NAMES =
exports.h = h;

TAG_NAMES.split('|').forEach(tag => {
// @ts-ignore
exports[tag] = hh.bind(null, tag);
});
20 changes: 20 additions & 0 deletions lib/typedefs.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component, ComponentChildren, ComponentType, VNode } from 'preact';
import p from 'preact';

declare function phy(
selectorOrComp: string | ComponentType,
...kids: ComponentChildren[]
): VNode;
declare function phy(
selectorOrComp: string | ComponentType,
attrs: Readonly<Record<string, any>>,
...kids: ComponentChildren[]
): VNode;

declare namespace phy {
let h: typeof phy;
let Component: typeof p.Component;
let render: typeof p.render;
}

export = phy;
26 changes: 22 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"scripts": {
"lint": "npm-run-all lint:*",
"lint:js": "eslint .",
"lint:tsc": "tsc",
"lint:typedefs": "prettier --check --single-quote lib/typedefs.d.ts",
"pretest": "npm-run-all pretest:*",
"test": "npm-run-all test:*",
"posttest": "npm-run-all posttest:*",
Expand All @@ -40,10 +42,13 @@
"functions": 100,
"statements": 100
},
"types": "lib/typedefs.d.ts",
"dependencies": {
"preact": "^10.0.0"
"preact": "^10.0.1"
},
"devDependencies": {
"@types/mocha": "^5.2.7",
"@types/node": "^8",
"assertive": "^2.1.0",
"eslint": "^6.2.1",
"eslint-config-groupon": "^7.2.0",
Expand All @@ -56,7 +61,8 @@
"npm-run-all": "^4.1.5",
"nyc": "^14.1.1",
"preact-render-to-string": "^5.0.6",
"prettier": "^1.18.2"
"prettier": "^1.18.2",
"typescript": "^3.6.2"
},
"author": {
"name": "David Bushong",
Expand Down
4 changes: 2 additions & 2 deletions test/phy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ const tests = [
[
'id, className attr',
h('#bar', { className: 'yadda' }),
'<div class="yadda" id="bar"></div>',
'<div id="bar" class="yadda"></div>',
],
[
'id, multiple classes, class attribute',
h('.foo.bar#baz', { class: 'garply quux' }),
'<div class="garply quux foo bar" id="baz"></div>',
'<div id="baz" class="garply quux foo bar"></div>',
],
['tag, string kids', h('div', 'kittens'), '<div>kittens</div>'],
[
Expand Down
23 changes: 23 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"allowJs": true,
"checkJs": true,
"noEmit": true,
"resolveJsonModule": true,
"strict": true,
"moduleResolution": "node",
"types": [
"node",
"mocha"
],
"esModuleInterop": true
},
"exclude": [],
"include": [
"typings/**/*.d.ts",
"*.js",
"lib/**/*.js"
]
}

0 comments on commit 403b571

Please sign in to comment.