Skip to content

Commit 8f31c38

Browse files
wooormremcohaszing
andauthored
Add automatic, classic runtime types
Closes GH-9 Reviewed-by: Christian Murphy <christian.murphy.42@gmail.com> Co-authored-by: Remco Haszing <remcohaszing@gmail.com>
1 parent a90a4e3 commit 8f31c38

17 files changed

+315
-14
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
.DS_Store
2-
*.d.ts
32
*.log
43
coverage/
54
node_modules/
65
test/jsx-*.js
76
yarn.lock
7+
/*.d.ts
8+
test/*.d.ts
9+
script/*.d.ts
10+
lib/*.d.ts
11+
!lib/jsx-automatic.d.ts
12+
!lib/jsx-classic.d.ts

index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1+
/**
2+
* @typedef {import('./lib/index.js').XChild}} Child Acceptable child value
3+
* @typedef {import('./lib/index.js').XAttributes}} Attributes Acceptable attributes value.
4+
*/
5+
16
export {x} from './lib/index.js'

jsx-runtime.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* @typedef {import('./lib/runtime.js').JSXProps}} JSXProps
3+
*/
4+
5+
export * from './lib/runtime.js'

lib/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
* @typedef {string|number|null|undefined} XPrimitiveChild
1111
* @typedef {Array.<Node|XPrimitiveChild>} XArrayChild
1212
* @typedef {Node|XPrimitiveChild|XArrayChild} XChild
13+
* @typedef {import('./jsx-classic').Element} x.JSX.Element
14+
* @typedef {import('./jsx-classic').IntrinsicAttributes} x.JSX.IntrinsicAttributes
15+
* @typedef {import('./jsx-classic').IntrinsicElements} x.JSX.IntrinsicElements
16+
* @typedef {import('./jsx-classic').ElementChildrenAttribute} x.JSX.ElementChildrenAttribute
1317
*/
1418

1519
/**

lib/jsx-automatic.d.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {XAttributes, XChild, XResult} from './index.js'
2+
3+
export namespace JSX {
4+
/**
5+
* This defines the return value of JSX syntax.
6+
*/
7+
type Element = XResult
8+
9+
/**
10+
* This disallows the use of functional components.
11+
*/
12+
type IntrinsicAttributes = never
13+
14+
/**
15+
* This defines the prop types for known elements.
16+
*
17+
* For `xastscript` this defines any string may be used in combination with `xast` `Attributes`.
18+
*
19+
* This **must** be an interface.
20+
*/
21+
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
22+
interface IntrinsicElements {
23+
[name: string]:
24+
| XAttributes
25+
| {
26+
/**
27+
* The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type.
28+
*/
29+
children?: XChild
30+
}
31+
}
32+
33+
/**
34+
* The key of this interface defines as what prop children are passed.
35+
*/
36+
interface ElementChildrenAttribute {
37+
/**
38+
* Only the key matters, not the value.
39+
*/
40+
children?: never
41+
}
42+
}

lib/jsx-automatic.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Empty (only used for TypeScript).
2+
export {}

lib/jsx-classic.d.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {XAttributes, XChild, XResult} from './index.js'
2+
3+
/**
4+
* This unique symbol is declared to specify the key on which JSX children are passed, without conflicting
5+
* with the Attributes type.
6+
*/
7+
declare const children: unique symbol
8+
9+
/**
10+
* This defines the return value of JSX syntax.
11+
*/
12+
export type Element = XResult
13+
14+
/**
15+
* This disallows the use of functional components.
16+
*/
17+
export type IntrinsicAttributes = never
18+
19+
/**
20+
* This defines the prop types for known elements.
21+
*
22+
* For `xastscript` this defines any string may be used in combination with `xast` `Attributes`.
23+
*
24+
* This **must** be an interface.
25+
*/
26+
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
27+
export interface IntrinsicElements {
28+
[name: string]:
29+
| XAttributes
30+
| {
31+
/**
32+
* The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type.
33+
*/
34+
[children]?: XChild
35+
}
36+
}
37+
38+
/**
39+
* The key of this interface defines as what prop children are passed.
40+
*/
41+
export interface ElementChildrenAttribute {
42+
/**
43+
* Only the key matters, not the value.
44+
*/
45+
[children]?: never
46+
}

lib/jsx-classic.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Empty (only used for TypeScript).
2+
export {}

lib/runtime.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @typedef {import('./index.js').Element} Element
3+
* @typedef {import('./index.js').Root} Root
4+
* @typedef {import('./index.js').XResult} XResult
5+
* @typedef {import('./index.js').XChild} XChild
6+
* @typedef {import('./index.js').XAttributes} XAttributes
7+
* @typedef {import('./index.js').XValue} XValue
8+
*
9+
* @typedef {{[x: string]: XValue|XChild}} JSXProps
10+
*/
11+
12+
import {x} from './index.js'
13+
14+
// Export `JSX` as a global for TypeScript.
15+
export * from './jsx-automatic.js'
16+
17+
/**
18+
* Create XML trees in xast through JSX.
19+
*
20+
* @param name Qualified name. Case sensitive and can contain a namespace prefix (such as `rdf:RDF`). Pass `null|undefined` to build a root.
21+
* @param props Map of attributes. Nullish (null or undefined) or NaN values are ignored, other values (strings, booleans) are cast to strings. `children` can contain one child or a list of children. When strings are encountered, they are mapped to text nodes.
22+
*/
23+
export const jsx =
24+
/**
25+
* @type {{
26+
* (name: null|undefined, props: {children?: XChild}, key?: string): Root
27+
* (name: string, props: JSXProps, key?: string): Element
28+
* }}
29+
*/
30+
(
31+
/**
32+
* @param {string|null} name
33+
* @param {XAttributes & {children?: XChild}} props
34+
* @returns {XResult}
35+
*/
36+
function (name, props) {
37+
var {children, ...properties} = props
38+
return name === null ? x(name, children) : x(name, properties, children)
39+
}
40+
)
41+
42+
export const jsxs = jsx
43+
44+
/** @type {null} */
45+
export const Fragment = null

package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,18 @@
3434
"index.d.ts",
3535
"index.js"
3636
],
37+
"exports": {
38+
".": "./index.js",
39+
"./index.js": "./index.js",
40+
"./jsx-runtime": "./jsx-runtime.js"
41+
},
3742
"dependencies": {
3843
"@types/xast": "^1.0.0"
3944
},
4045
"devDependencies": {
4146
"@babel/core": "^7.0.0",
4247
"@babel/plugin-syntax-jsx": "^7.0.0",
4348
"@babel/plugin-transform-react-jsx": "^7.0.0",
44-
"@types/acorn": "^4.0.0",
4549
"@types/babel__core": "^7.0.0",
4650
"@types/tape": "^4.0.0",
4751
"astring": "^1.0.0",
@@ -61,7 +65,7 @@
6165
},
6266
"scripts": {
6367
"prepack": "npm run build && npm run format",
64-
"build": "rimraf \"{script/**,test/**,lib/**,}*.d.ts\" && tsc && tsd && type-coverage",
68+
"build": "rimraf \"{script/**,test/**,}*.d.ts\" \"lib/{index,runtime}.d.ts\" && tsc && tsd && type-coverage",
6569
"generate": "node script/generate-jsx",
6670
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
6771
"test-api": "node test/index.js",
@@ -81,10 +85,7 @@
8185
"rules": {
8286
"no-var": "off",
8387
"prefer-arrow-callback": "off"
84-
},
85-
"ignore": [
86-
"types/"
87-
]
88+
}
8889
},
8990
"remarkConfig": {
9091
"plugins": [

script/generate-jsx.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {buildJsx} from 'estree-util-build-jsx'
99
var doc = String(fs.readFileSync(path.join('test', 'jsx.jsx')))
1010

1111
fs.writeFileSync(
12-
path.join('test', 'jsx-build-jsx.js'),
12+
path.join('test', 'jsx-build-jsx-classic.js'),
1313
generate(
1414
buildJsx(
1515
// @ts-ignore Acorn nodes are assignable to ESTree nodes.
@@ -24,11 +24,41 @@ fs.writeFileSync(
2424
)
2525

2626
fs.writeFileSync(
27-
path.join('test', 'jsx-babel.js'),
27+
path.join('test', 'jsx-build-jsx-automatic.js'),
28+
generate(
29+
buildJsx(
30+
// @ts-ignore Acorn nodes are assignable to ESTree nodes.
31+
Parser.extend(acornJsx()).parse(
32+
doc.replace(/'name'/, "'jsx (estree-util-build-jsx, automatic)'"),
33+
// @ts-ignore Hush, `2021` is fine.
34+
{sourceType: 'module', ecmaVersion: 2021}
35+
),
36+
{runtime: 'automatic', importSource: '.'}
37+
)
38+
).replace(/\/jsx-runtime(?=["'])/g, './lib/runtime.js')
39+
)
40+
41+
fs.writeFileSync(
42+
path.join('test', 'jsx-babel-classic.js'),
2843
// @ts-ignore Result always given.
2944
babel.transform(doc.replace(/'name'/, "'jsx (babel, classic)'"), {
3045
plugins: [
3146
['@babel/plugin-transform-react-jsx', {pragma: 'x', pragmaFrag: 'null'}]
3247
]
3348
}).code
3449
)
50+
51+
fs.writeFileSync(
52+
path.join('test', 'jsx-babel-automatic.js'),
53+
// @ts-ignore Result always given.
54+
babel
55+
.transformSync(doc.replace(/'name'/, "'jsx (babel, automatic)'"), {
56+
plugins: [
57+
[
58+
'@babel/plugin-transform-react-jsx',
59+
{runtime: 'automatic', importSource: '.'}
60+
]
61+
]
62+
})
63+
.code.replace(/\/jsx-runtime(?=["'])/g, './lib/runtime.js')
64+
)

test-d/automatic.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* @jsxRuntime automatic */
2+
/* @jsxImportSource .. */
3+
4+
import {expectType, expectError} from 'tsd'
5+
import {Root, Element} from 'xast'
6+
import {x} from '../index.js'
7+
import {Fragment, jsx, jsxs} from '../jsx-runtime.js'
8+
9+
type Result = Element | Root
10+
11+
// JSX automatic runtime.
12+
expectType<Root>(jsx(Fragment, {}))
13+
expectType<Root>(jsx(Fragment, {children: x('x')}))
14+
expectType<Element>(jsx('a', {}))
15+
expectType<Element>(jsx('a', {children: 'a'}))
16+
expectType<Element>(jsx('a', {children: x('x')}))
17+
expectType<Element>(jsxs('a', {children: ['a', 'b']}))
18+
expectType<Element>(jsxs('a', {children: [x('x'), x('y')]}))
19+
20+
expectType<Result>(<></>)
21+
expectType<Result>(<a />)
22+
expectType<Result>(<a b="c" />)
23+
expectType<Result>(<a b={'c'} />)
24+
expectType<Result>(<a>string</a>)
25+
expectType<Result>(<a>{['string', 'string']}</a>)
26+
expectType<Result>(
27+
<a>
28+
<></>
29+
</a>
30+
)
31+
expectType<Result>(<a>{x()}</a>)
32+
expectType<Result>(<a>{x('b')}</a>)
33+
expectType<Result>(
34+
<a>
35+
<b />c
36+
</a>
37+
)
38+
expectType<Result>(
39+
<a>
40+
<b />
41+
<c />
42+
</a>
43+
)
44+
expectType<Result>(<a>{[<b />, <c />]}</a>)
45+
expectType<Result>(<a>{[<b />, <c />]}</a>)
46+
expectType<Result>(<a>{[]}</a>)
47+
48+
expectError(<a invalid={{}} />)
49+
expectError(<a invalid={[1]} />)
50+
expectError(<a>{{invalid: 'child'}}</a>)
51+
52+
// This is where the automatic runtime differs from the classic runtime.
53+
// The automatic runtime the children prop to define JSX children, whereas it’s used as an attribute in the classic runtime.
54+
expectType<Result>(<a children={<b />} />)
55+
56+
declare function Bar(props?: Record<string, unknown>): Element
57+
expectError(<Bar />)

test-d/classic.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/* @jsx x */
2+
/* @jsxFrag null */
3+
import {expectType, expectError} from 'tsd'
4+
import {Root, Element} from 'xast'
5+
import {x} from '../index.js'
6+
7+
type Result = Element | Root
8+
9+
expectType<Result>(<></>)
10+
expectType<Result>(<a />)
11+
expectType<Result>(<a b="c" />)
12+
expectType<Result>(<a b={'c'} />)
13+
expectType<Result>(<a>string</a>)
14+
expectType<Result>(<a>{['string', 'string']}</a>)
15+
expectType<Result>(
16+
<a>
17+
<></>
18+
</a>
19+
)
20+
expectType<Result>(<a>{x()}</a>)
21+
expectType<Result>(<a>{x('b')}</a>)
22+
expectType<Result>(
23+
<a>
24+
<b />c
25+
</a>
26+
)
27+
expectType<Result>(
28+
<a>
29+
<b />
30+
<c />
31+
</a>
32+
)
33+
expectType<Result>(<a>{[<b />, <c />]}</a>)
34+
expectType<Result>(<a>{[<b />, <c />]}</a>)
35+
expectType<Result>(<a>{[]}</a>)
36+
37+
expectError(<a invalid={{}} />)
38+
expectError(<a invalid={[1]} />)
39+
expectError(<a>{{invalid: 'child'}}</a>)
40+
41+
// This is where the classic runtime differs from the automatic runtime.
42+
// The automatic runtime the children prop to define JSX children, whereas it’s
43+
// used as an attribute in the classic runtime.
44+
expectError(<a children={<b />} />)
45+
46+
declare function Bar(props?: Record<string, unknown>): Element
47+
expectError(<Bar />)

index.test-d.ts renamed to test-d/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {expectType, expectError} from 'tsd'
22
import {Root, Element} from 'xast'
3-
import {x} from './index.js'
3+
import {x} from '../index.js'
44

55
expectType<Root>(x())
66
expectError(x(true))

test/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable import/no-unassigned-import */
22
import './core.js'
3-
import './jsx-babel.js'
4-
import './jsx-build-jsx.js'
3+
import './jsx-babel-classic.js'
4+
import './jsx-babel-automatic.js'
5+
import './jsx-build-jsx-classic.js'
6+
import './jsx-build-jsx-automatic.js'
57
/* eslint-enable import/no-unassigned-import */

0 commit comments

Comments
 (0)