Skip to content

Commit 3f8476b

Browse files
committed
wip: add automatic, classic runtime types
1 parent a90a4e3 commit 3f8476b

17 files changed

+316
-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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* @typedef {Node|XPrimitiveChild|XArrayChild} XChild
1313
*/
1414

15+
export * from './jsx-classic.js'
16+
1517
/**
1618
* Create XML trees in xast.
1719
*

lib/jsx-automatic.d.ts

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

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

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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
declare function Bar(props?: Record<string, unknown>): Element
53+
expectError(<Bar />)

test-d/classic.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
// To do: fix classic types.
10+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
11+
expectType<Result>(<></>)
12+
expectType<Result>(<a />)
13+
expectType<Result>(<a b="c" />)
14+
expectType<Result>(<a b={'c'} />)
15+
expectType<Result>(<a>string</a>)
16+
expectType<Result>(<a>{['string', 'string']}</a>)
17+
expectType<Result>(
18+
<a>
19+
<></>
20+
</a>
21+
)
22+
expectType<Result>(<a>{x()}</a>)
23+
expectType<Result>(<a>{x('b')}</a>)
24+
expectType<Result>(
25+
<a>
26+
<b />c
27+
</a>
28+
)
29+
expectType<Result>(
30+
<a>
31+
<b />
32+
<c />
33+
</a>
34+
)
35+
expectType<Result>(<a>{[<b />, <c />]}</a>)
36+
expectType<Result>(<a>{[<b />, <c />]}</a>)
37+
expectType<Result>(<a>{[]}</a>)
38+
39+
expectError(<a invalid={{}} />)
40+
expectError(<a invalid={[1]} />)
41+
expectError(<a>{{invalid: 'child'}}</a>)
42+
43+
declare function Bar(props?: Record<string, unknown>): Element
44+
expectError(<Bar />)
45+
46+
/* eslint-enable @typescript-eslint/no-unsafe-argument */

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)