diff --git a/.gitignore b/.gitignore index 4a71111..657b349 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,12 @@ .DS_Store -*.d.ts *.log coverage/ node_modules/ test/jsx-*.js yarn.lock +/*.d.ts +test/*.d.ts +script/*.d.ts +lib/*.d.ts +!lib/jsx-automatic.d.ts +!lib/jsx-classic.d.ts diff --git a/index.js b/index.js index 477e338..90a4fc9 100644 --- a/index.js +++ b/index.js @@ -1 +1,6 @@ +/** + * @typedef {import('./lib/index.js').XChild}} Child Acceptable child value + * @typedef {import('./lib/index.js').XAttributes}} Attributes Acceptable attributes value. + */ + export {x} from './lib/index.js' diff --git a/jsx-runtime.js b/jsx-runtime.js new file mode 100644 index 0000000..8049d04 --- /dev/null +++ b/jsx-runtime.js @@ -0,0 +1,5 @@ +/** + * @typedef {import('./lib/runtime.js').JSXProps}} JSXProps + */ + +export * from './lib/runtime.js' diff --git a/lib/index.js b/lib/index.js index aa0b17d..6af9859 100644 --- a/lib/index.js +++ b/lib/index.js @@ -10,6 +10,10 @@ * @typedef {string|number|null|undefined} XPrimitiveChild * @typedef {Array.} XArrayChild * @typedef {Node|XPrimitiveChild|XArrayChild} XChild + * @typedef {import('./jsx-classic').Element} x.JSX.Element + * @typedef {import('./jsx-classic').IntrinsicAttributes} x.JSX.IntrinsicAttributes + * @typedef {import('./jsx-classic').IntrinsicElements} x.JSX.IntrinsicElements + * @typedef {import('./jsx-classic').ElementChildrenAttribute} x.JSX.ElementChildrenAttribute */ /** diff --git a/lib/jsx-automatic.d.ts b/lib/jsx-automatic.d.ts new file mode 100644 index 0000000..3c45020 --- /dev/null +++ b/lib/jsx-automatic.d.ts @@ -0,0 +1,42 @@ +import {XAttributes, XChild, XResult} from './index.js' + +export namespace JSX { + /** + * This defines the return value of JSX syntax. + */ + type Element = XResult + + /** + * This disallows the use of functional components. + */ + type IntrinsicAttributes = never + + /** + * This defines the prop types for known elements. + * + * For `xastscript` this defines any string may be used in combination with `xast` `Attributes`. + * + * This **must** be an interface. + */ + // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style + interface IntrinsicElements { + [name: string]: + | XAttributes + | { + /** + * The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type. + */ + children?: XChild + } + } + + /** + * The key of this interface defines as what prop children are passed. + */ + interface ElementChildrenAttribute { + /** + * Only the key matters, not the value. + */ + children?: never + } +} diff --git a/lib/jsx-automatic.js b/lib/jsx-automatic.js new file mode 100644 index 0000000..a7f5817 --- /dev/null +++ b/lib/jsx-automatic.js @@ -0,0 +1,2 @@ +// Empty (only used for TypeScript). +export {} diff --git a/lib/jsx-classic.d.ts b/lib/jsx-classic.d.ts new file mode 100644 index 0000000..1e1e0a0 --- /dev/null +++ b/lib/jsx-classic.d.ts @@ -0,0 +1,46 @@ +import {XAttributes, XChild, XResult} from './index.js' + +/** + * This unique symbol is declared to specify the key on which JSX children are passed, without conflicting + * with the Attributes type. + */ +declare const children: unique symbol + +/** + * This defines the return value of JSX syntax. + */ +export type Element = XResult + +/** + * This disallows the use of functional components. + */ +export type IntrinsicAttributes = never + +/** + * This defines the prop types for known elements. + * + * For `xastscript` this defines any string may be used in combination with `xast` `Attributes`. + * + * This **must** be an interface. + */ +// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style +export interface IntrinsicElements { + [name: string]: + | XAttributes + | { + /** + * The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type. + */ + [children]?: XChild + } +} + +/** + * The key of this interface defines as what prop children are passed. + */ +export interface ElementChildrenAttribute { + /** + * Only the key matters, not the value. + */ + [children]?: never +} diff --git a/lib/jsx-classic.js b/lib/jsx-classic.js new file mode 100644 index 0000000..a7f5817 --- /dev/null +++ b/lib/jsx-classic.js @@ -0,0 +1,2 @@ +// Empty (only used for TypeScript). +export {} diff --git a/lib/runtime.js b/lib/runtime.js new file mode 100644 index 0000000..b608d4e --- /dev/null +++ b/lib/runtime.js @@ -0,0 +1,45 @@ +/** + * @typedef {import('./index.js').Element} Element + * @typedef {import('./index.js').Root} Root + * @typedef {import('./index.js').XResult} XResult + * @typedef {import('./index.js').XChild} XChild + * @typedef {import('./index.js').XAttributes} XAttributes + * @typedef {import('./index.js').XValue} XValue + * + * @typedef {{[x: string]: XValue|XChild}} JSXProps + */ + +import {x} from './index.js' + +// Export `JSX` as a global for TypeScript. +export * from './jsx-automatic.js' + +/** + * Create XML trees in xast through JSX. + * + * @param name Qualified name. Case sensitive and can contain a namespace prefix (such as `rdf:RDF`). Pass `null|undefined` to build a root. + * @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. + */ +export const jsx = + /** + * @type {{ + * (name: null|undefined, props: {children?: XChild}, key?: string): Root + * (name: string, props: JSXProps, key?: string): Element + * }} + */ + ( + /** + * @param {string|null} name + * @param {XAttributes & {children?: XChild}} props + * @returns {XResult} + */ + function (name, props) { + var {children, ...properties} = props + return name === null ? x(name, children) : x(name, properties, children) + } + ) + +export const jsxs = jsx + +/** @type {null} */ +export const Fragment = null diff --git a/package.json b/package.json index 2dbbe14..f9dbd6c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,11 @@ "index.d.ts", "index.js" ], + "exports": { + ".": "./index.js", + "./index.js": "./index.js", + "./jsx-runtime": "./jsx-runtime.js" + }, "dependencies": { "@types/xast": "^1.0.0" }, @@ -41,7 +46,6 @@ "@babel/core": "^7.0.0", "@babel/plugin-syntax-jsx": "^7.0.0", "@babel/plugin-transform-react-jsx": "^7.0.0", - "@types/acorn": "^4.0.0", "@types/babel__core": "^7.0.0", "@types/tape": "^4.0.0", "astring": "^1.0.0", @@ -61,7 +65,7 @@ }, "scripts": { "prepack": "npm run build && npm run format", - "build": "rimraf \"{script/**,test/**,lib/**,}*.d.ts\" && tsc && tsd && type-coverage", + "build": "rimraf \"{script/**,test/**,}*.d.ts\" \"lib/{index,runtime}.d.ts\" && tsc && tsd && type-coverage", "generate": "node script/generate-jsx", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", "test-api": "node test/index.js", @@ -81,10 +85,7 @@ "rules": { "no-var": "off", "prefer-arrow-callback": "off" - }, - "ignore": [ - "types/" - ] + } }, "remarkConfig": { "plugins": [ diff --git a/script/generate-jsx.js b/script/generate-jsx.js index 1fdf329..04921dd 100644 --- a/script/generate-jsx.js +++ b/script/generate-jsx.js @@ -9,7 +9,7 @@ import {buildJsx} from 'estree-util-build-jsx' var doc = String(fs.readFileSync(path.join('test', 'jsx.jsx'))) fs.writeFileSync( - path.join('test', 'jsx-build-jsx.js'), + path.join('test', 'jsx-build-jsx-classic.js'), generate( buildJsx( // @ts-ignore Acorn nodes are assignable to ESTree nodes. @@ -24,7 +24,22 @@ fs.writeFileSync( ) fs.writeFileSync( - path.join('test', 'jsx-babel.js'), + path.join('test', 'jsx-build-jsx-automatic.js'), + generate( + buildJsx( + // @ts-ignore Acorn nodes are assignable to ESTree nodes. + Parser.extend(acornJsx()).parse( + doc.replace(/'name'/, "'jsx (estree-util-build-jsx, automatic)'"), + // @ts-ignore Hush, `2021` is fine. + {sourceType: 'module', ecmaVersion: 2021} + ), + {runtime: 'automatic', importSource: '.'} + ) + ).replace(/\/jsx-runtime(?=["'])/g, './lib/runtime.js') +) + +fs.writeFileSync( + path.join('test', 'jsx-babel-classic.js'), // @ts-ignore Result always given. babel.transform(doc.replace(/'name'/, "'jsx (babel, classic)'"), { plugins: [ @@ -32,3 +47,18 @@ fs.writeFileSync( ] }).code ) + +fs.writeFileSync( + path.join('test', 'jsx-babel-automatic.js'), + // @ts-ignore Result always given. + babel + .transformSync(doc.replace(/'name'/, "'jsx (babel, automatic)'"), { + plugins: [ + [ + '@babel/plugin-transform-react-jsx', + {runtime: 'automatic', importSource: '.'} + ] + ] + }) + .code.replace(/\/jsx-runtime(?=["'])/g, './lib/runtime.js') +) diff --git a/test-d/automatic.tsx b/test-d/automatic.tsx new file mode 100644 index 0000000..7203678 --- /dev/null +++ b/test-d/automatic.tsx @@ -0,0 +1,57 @@ +/* @jsxRuntime automatic */ +/* @jsxImportSource .. */ + +import {expectType, expectError} from 'tsd' +import {Root, Element} from 'xast' +import {x} from '../index.js' +import {Fragment, jsx, jsxs} from '../jsx-runtime.js' + +type Result = Element | Root + +// JSX automatic runtime. +expectType(jsx(Fragment, {})) +expectType(jsx(Fragment, {children: x('x')})) +expectType(jsx('a', {})) +expectType(jsx('a', {children: 'a'})) +expectType(jsx('a', {children: x('x')})) +expectType(jsxs('a', {children: ['a', 'b']})) +expectType(jsxs('a', {children: [x('x'), x('y')]})) + +expectType(<>) +expectType() +expectType() +expectType() +expectType(string) +expectType({['string', 'string']}) +expectType( + + <> + +) +expectType({x()}) +expectType({x('b')}) +expectType( + + c + +) +expectType( + + + + +) +expectType({[, ]}) +expectType({[, ]}) +expectType({[]}) + +expectError() +expectError() +expectError({{invalid: 'child'}}) + +// This is where the automatic runtime differs from the classic runtime. +// The automatic runtime the children prop to define JSX children, whereas it’s used as an attribute in the classic runtime. +expectType(} />) + +declare function Bar(props?: Record): Element +expectError() diff --git a/test-d/classic.tsx b/test-d/classic.tsx new file mode 100644 index 0000000..c880686 --- /dev/null +++ b/test-d/classic.tsx @@ -0,0 +1,47 @@ +/* @jsx x */ +/* @jsxFrag null */ +import {expectType, expectError} from 'tsd' +import {Root, Element} from 'xast' +import {x} from '../index.js' + +type Result = Element | Root + +expectType(<>) +expectType() +expectType() +expectType() +expectType(string) +expectType({['string', 'string']}) +expectType( + + <> + +) +expectType({x()}) +expectType({x('b')}) +expectType( + + c + +) +expectType( + + + + +) +expectType({[, ]}) +expectType({[, ]}) +expectType({[]}) + +expectError() +expectError() +expectError({{invalid: 'child'}}) + +// This is where the classic runtime differs from the automatic runtime. +// The automatic runtime the children prop to define JSX children, whereas it’s +// used as an attribute in the classic runtime. +expectError(} />) + +declare function Bar(props?: Record): Element +expectError() diff --git a/index.test-d.ts b/test-d/index.tsx similarity index 96% rename from index.test-d.ts rename to test-d/index.tsx index a091b92..a62cace 100644 --- a/index.test-d.ts +++ b/test-d/index.tsx @@ -1,6 +1,6 @@ import {expectType, expectError} from 'tsd' import {Root, Element} from 'xast' -import {x} from './index.js' +import {x} from '../index.js' expectType(x()) expectError(x(true)) diff --git a/test/index.js b/test/index.js index 6fcc935..b6b160d 100644 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,7 @@ /* eslint-disable import/no-unassigned-import */ import './core.js' -import './jsx-babel.js' -import './jsx-build-jsx.js' +import './jsx-babel-classic.js' +import './jsx-babel-automatic.js' +import './jsx-build-jsx-classic.js' +import './jsx-build-jsx-automatic.js' /* eslint-enable import/no-unassigned-import */ diff --git a/test/jsx.jsx b/test/jsx.jsx index 045c5d3..7774e5b 100644 --- a/test/jsx.jsx +++ b/test/jsx.jsx @@ -85,7 +85,7 @@ test('name', function (t) { {1 + 1} , x('a', [x('b'), 'c', x('d', 'e'), '2']), - 'should support text, elements, and expressions in jsx' + 'should support text, elements, and expressions in JSX' ) t.deepEqual( diff --git a/tsconfig.json b/tsconfig.json index c0a7804..60106cb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,13 @@ { - "include": ["*.js", "script/**/*.js", "test/**/*.js", "lib/**/*.js"], + "include": [ + "*.js", + "script/**/*.js", + "test/**/*.js", + "lib/index.js", + "lib/runtime.js", + "lib/jsx-automatic.d.ts", + "lib/jsx-classic.d.ts" + ], "compilerOptions": { "target": "ES2020", "lib": ["ES2020"],