Skip to content

Commit a90a4e3

Browse files
committed
Add JSDoc based types
1 parent 65ecfd2 commit a90a4e3

14 files changed

+194
-275
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
2+
*.d.ts
23
*.log
34
coverage/
45
node_modules/

index.js

Lines changed: 1 addition & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1 @@
1-
// Creating xast elements.
2-
export function x(name, attributes) {
3-
var node =
4-
name === undefined || name === null
5-
? {type: 'root', children: []}
6-
: {type: 'element', name, attributes: {}, children: []}
7-
var index = 1
8-
var key
9-
10-
if (name !== undefined && name !== null && typeof name !== 'string') {
11-
throw new Error('Expected element name, got `' + name + '`')
12-
}
13-
14-
// Handle props.
15-
if (attributes) {
16-
if (
17-
name === undefined ||
18-
name === null ||
19-
typeof attributes === 'string' ||
20-
typeof attributes === 'number' ||
21-
'length' in attributes
22-
) {
23-
// Nope, it’s something for `children`.
24-
index--
25-
} else {
26-
for (key in attributes) {
27-
// Ignore nullish and NaN values.
28-
if (
29-
attributes[key] !== undefined &&
30-
attributes[key] !== null &&
31-
(typeof attributes[key] !== 'number' ||
32-
!Number.isNaN(attributes[key]))
33-
) {
34-
node.attributes[key] = String(attributes[key])
35-
}
36-
}
37-
}
38-
}
39-
40-
// Handle children.
41-
while (++index < arguments.length) {
42-
addChild(node.children, arguments[index])
43-
}
44-
45-
return node
46-
}
47-
48-
function addChild(nodes, value) {
49-
var index = -1
50-
51-
if (value === undefined || value === null) {
52-
// Empty.
53-
} else if (typeof value === 'string' || typeof value === 'number') {
54-
nodes.push({type: 'text', value: String(value)})
55-
} else if (typeof value === 'object' && 'length' in value) {
56-
while (++index < value.length) {
57-
addChild(nodes, value[index])
58-
}
59-
} else if (typeof value === 'object' && value.type) {
60-
if (value.type === 'root') {
61-
addChild(nodes, value.children)
62-
} else {
63-
nodes.push(value)
64-
}
65-
} else {
66-
throw new TypeError('Expected node, nodes, string, got `' + value + '`')
67-
}
68-
}
1+
export {x} from './lib/index.js'

index.test-d.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {expectType, expectError} from 'tsd'
2+
import {Root, Element} from 'xast'
3+
import {x} from './index.js'
4+
5+
expectType<Root>(x())
6+
expectError(x(true))
7+
expectType<Root>(x(null))
8+
expectType<Root>(x(undefined))
9+
expectType<Element>(x(''))
10+
expectType<Element>(x('', null))
11+
expectType<Element>(x('', undefined))
12+
expectType<Element>(x('', 1))
13+
expectType<Element>(x('', 'a'))
14+
expectError(x('', true))
15+
expectType<Element>(x('', [1, 'a', null]))
16+
expectError(x('', [true]))
17+
18+
expectType<Element>(x('', {}))
19+
expectType<Element>(x('', {}, [1, 'a', null]))
20+
expectType<Element>(x('', {p: 1}))
21+
expectType<Element>(x('', {p: null}))
22+
expectType<Element>(x('', {p: undefined}))
23+
expectType<Element>(x('', {p: true}))
24+
expectType<Element>(x('', {p: false}))
25+
expectType<Element>(x('', {p: 'a'}))
26+
expectError(x('', {p: [1]}))
27+
expectError(x('', {p: [true]}))
28+
expectError(x('', {p: ['a']}))
29+
expectError(x('', {p: {x: true}}))

lib/index.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* @typedef {import('xast').Root} Root
3+
* @typedef {import('xast').Element} Element
4+
* @typedef {Root['children'][number]} Child
5+
* @typedef {Child|Root} Node
6+
* @typedef {Root|Element} XResult
7+
* @typedef {string|number|boolean|null|undefined} XValue
8+
* @typedef {{[attribute: string]: XValue}} XAttributes Attributes to support JS primitive types
9+
*
10+
* @typedef {string|number|null|undefined} XPrimitiveChild
11+
* @typedef {Array.<Node|XPrimitiveChild>} XArrayChild
12+
* @typedef {Node|XPrimitiveChild|XArrayChild} XChild
13+
*/
14+
15+
/**
16+
* Create XML trees in xast.
17+
*
18+
* @param name Qualified name. Case sensitive and can contain a namespace prefix (such as `rdf:RDF`). Pass `null|undefined` to build a root.
19+
* @param attributes Map of attributes. Nullish (null or undefined) or NaN values are ignored, other values (strings, booleans) are cast to strings.
20+
* @param children (Lists of) child nodes. When strings are encountered, they are mapped to Text nodes.
21+
*/
22+
export const x =
23+
/**
24+
* @type {{
25+
* (): Root
26+
* (name: null|undefined, ...children: XChild[]): Root
27+
* (name: string, attributes: XAttributes, ...children: XChild[]): Element
28+
* (name: string, ...children: XChild[]): Element
29+
* }}
30+
*/
31+
(
32+
/**
33+
* Hyperscript compatible DSL for creating virtual xast trees.
34+
*
35+
* @param {string|null} [name]
36+
* @param {XAttributes|XChild} [attributes]
37+
* @param {XChild[]} children
38+
* @returns {XResult}
39+
*/
40+
function (name, attributes, ...children) {
41+
var index = -1
42+
/** @type {XResult} */
43+
var node
44+
/** @type {string} */
45+
var key
46+
47+
if (name === undefined || name === null) {
48+
node = {type: 'root', children: []}
49+
// @ts-ignore Root builder doesn’t accept attributes.
50+
children.unshift(attributes)
51+
} else if (typeof name === 'string') {
52+
node = {type: 'element', name, attributes: {}, children: []}
53+
54+
if (isAttributes(attributes)) {
55+
for (key in attributes) {
56+
// Ignore nullish and NaN values.
57+
if (
58+
attributes[key] !== undefined &&
59+
attributes[key] !== null &&
60+
(typeof attributes[key] !== 'number' ||
61+
!Number.isNaN(attributes[key]))
62+
) {
63+
// @ts-ignore Pretty sure we just set it.
64+
node.attributes[key] = String(attributes[key])
65+
}
66+
}
67+
} else {
68+
children.unshift(attributes)
69+
}
70+
} else {
71+
throw new TypeError('Expected element name, got `' + name + '`')
72+
}
73+
74+
// Handle children.
75+
while (++index < children.length) {
76+
addChild(node.children, children[index])
77+
}
78+
79+
return node
80+
}
81+
)
82+
83+
/**
84+
* @param {Array.<Child>} nodes
85+
* @param {XChild} value
86+
*/
87+
function addChild(nodes, value) {
88+
var index = -1
89+
90+
if (value === undefined || value === null) {
91+
// Empty.
92+
} else if (typeof value === 'string' || typeof value === 'number') {
93+
nodes.push({type: 'text', value: String(value)})
94+
} else if (Array.isArray(value)) {
95+
while (++index < value.length) {
96+
addChild(nodes, value[index])
97+
}
98+
} else if (typeof value === 'object' && 'type' in value) {
99+
if (value.type === 'root') {
100+
addChild(nodes, value.children)
101+
} else {
102+
nodes.push(value)
103+
}
104+
} else {
105+
throw new TypeError('Expected node, nodes, string, got `' + value + '`')
106+
}
107+
}
108+
109+
/**
110+
* @param {XAttributes|XChild} value
111+
* @returns {value is XAttributes}
112+
*/
113+
function isAttributes(value) {
114+
if (
115+
value === null ||
116+
value === undefined ||
117+
typeof value !== 'object' ||
118+
Array.isArray(value)
119+
) {
120+
return false
121+
}
122+
123+
return true
124+
}

package.json

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
"sideEffects": false,
2929
"type": "module",
3030
"main": "index.js",
31-
"types": "types",
31+
"types": "index.d.ts",
3232
"files": [
33-
"types/index.d.ts",
33+
"lib/",
34+
"index.d.ts",
3435
"index.js"
3536
],
3637
"dependencies": {
@@ -40,23 +41,32 @@
4041
"@babel/core": "^7.0.0",
4142
"@babel/plugin-syntax-jsx": "^7.0.0",
4243
"@babel/plugin-transform-react-jsx": "^7.0.0",
44+
"@types/acorn": "^4.0.0",
45+
"@types/babel__core": "^7.0.0",
46+
"@types/tape": "^4.0.0",
4347
"astring": "^1.0.0",
4448
"buble": "^0.20.0",
4549
"c8": "^7.0.0",
4650
"estree-util-build-jsx": "^2.0.0",
4751
"prettier": "^2.0.0",
4852
"remark-cli": "^9.0.0",
4953
"remark-preset-wooorm": "^8.0.0",
54+
"rimraf": "^3.0.0",
5055
"tape": "^5.0.0",
56+
"tsd": "^0.14.0",
57+
"type-coverage": "^2.0.0",
58+
"typescript": "^4.0.0",
5159
"unist-builder": "^3.0.0",
5260
"xo": "^0.39.0"
5361
},
5462
"scripts": {
63+
"prepack": "npm run build && npm run format",
64+
"build": "rimraf \"{script/**,test/**,lib/**,}*.d.ts\" && tsc && tsd && type-coverage",
5565
"generate": "node script/generate-jsx",
5666
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
5767
"test-api": "node test/index.js",
5868
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test/index.js",
59-
"test": "npm run generate && npm run format && npm run test-coverage"
69+
"test": "npm run generate && npm run build && npm run format && npm run test-coverage"
6070
},
6171
"prettier": {
6272
"tabWidth": 2,
@@ -80,5 +90,10 @@
8090
"plugins": [
8191
"preset-wooorm"
8292
]
93+
},
94+
"typeCoverage": {
95+
"atLeast": 100,
96+
"detail": true,
97+
"strict": true
8398
}
8499
}

readme.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,7 @@ For more details on configuring JSX for TypeScript, see the
241241
TypeScript also lets you configure this in a script:
242242

243243
```tsx
244-
/** @jsx x */
245-
/** @jsxFrag null */
244+
/** @jsx x @jsxFrag null */
246245
import {x} from 'xastscript'
247246

248247
console.log(<music />)

script/generate-jsx.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ fs.writeFileSync(
1515
// @ts-ignore Acorn nodes are assignable to ESTree nodes.
1616
Parser.extend(acornJsx()).parse(
1717
doc.replace(/'name'/, "'jsx (estree-util-build-jsx, classic)'"),
18+
// @ts-ignore Hush, `2021` is fine.
1819
{sourceType: 'module', ecmaVersion: 2021}
1920
),
2021
{pragma: 'x', pragmaFrag: 'null'}

test/core.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ test('xastscript', function (t) {
1212

1313
t.throws(
1414
function () {
15+
// @ts-ignore runtime.
1516
x(1)
1617
},
1718
/Expected element name, got `1`/,
@@ -68,6 +69,7 @@ test('xastscript', function (t) {
6869
)
6970

7071
t.deepEqual(
72+
// @ts-ignore Deeply nested children are not typed.
7173
x('y', {}, [[[x('a')]], [[[[x('b')]], x('c')]]]),
7274
{
7375
type: 'element',
@@ -118,6 +120,7 @@ test('xastscript', function (t) {
118120

119121
t.throws(
120122
function () {
123+
// @ts-ignore runtime.
121124
x('y', {}, {})
122125
},
123126
/Expected node, nodes, string, got `\[object Object]`/,

tsconfig.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"include": ["*.js", "script/**/*.js", "test/**/*.js", "lib/**/*.js"],
3+
"compilerOptions": {
4+
"target": "ES2020",
5+
"lib": ["ES2020"],
6+
"module": "ES2020",
7+
"moduleResolution": "node",
8+
"allowJs": true,
9+
"checkJs": true,
10+
"declaration": true,
11+
"emitDeclarationOnly": true,
12+
"allowSyntheticDefaultImports": true,
13+
"skipLibCheck": true,
14+
"strictNullChecks": true
15+
}
16+
}

0 commit comments

Comments
 (0)