Skip to content

Commit 0c6d8ac

Browse files
mdynnlwooorm
andauthored
Fix generated JSX for custom elements
Related to: ddcebdb. Related to: remarkjs/remark-math#72. Closes GH-106. Co-authored-by: Titus Wormer <[email protected]>
1 parent adae4b2 commit 0c6d8ac

4 files changed

Lines changed: 80 additions & 15 deletions

File tree

lib/plugin/recma-jsx-rewrite.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,10 @@ export function recmaJsxRewrite(options = {}) {
175175
fnScope.tags.push(id)
176176
}
177177

178-
node.openingElement.name = {
179-
type: 'JSXMemberExpression',
180-
object: {type: 'JSXIdentifier', name: '_components'},
181-
property: name
182-
}
178+
node.openingElement.name = toJsxIdOrMemberExpression([
179+
'_components',
180+
id
181+
])
183182

184183
if (node.closingElement) {
185184
node.closingElement.name = toJsxIdOrMemberExpression([

lib/util/estree-util-to-id-or-member-expression.js

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,35 @@
66
* @typedef {import('estree-jsx').JSXMemberExpression} JSXMemberExpression
77
*/
88

9-
import {name as isIdentifierName} from 'estree-util-is-identifier-name'
9+
import {
10+
start as esStart,
11+
cont as esCont,
12+
name as isIdentifierName
13+
} from 'estree-util-is-identifier-name'
1014

1115
export const toIdOrMemberExpression = toIdOrMemberExpressionFactory(
1216
'Identifier',
13-
'MemberExpression'
17+
'MemberExpression',
18+
isIdentifierName
1419
)
1520

1621
export const toJsxIdOrMemberExpression =
1722
// @ts-expect-error: fine
1823
/** @type {(ids: Array<string|number>) => JSXIdentifier|JSXMemberExpression)} */
19-
(toIdOrMemberExpressionFactory('JSXIdentifier', 'JSXMemberExpression'))
24+
(
25+
toIdOrMemberExpressionFactory(
26+
'JSXIdentifier',
27+
'JSXMemberExpression',
28+
isJsxIdentifierName
29+
)
30+
)
2031

2132
/**
22-
* @param {string} [idType]
23-
* @param {string} [memberType]
33+
* @param {string} idType
34+
* @param {string} memberType
35+
* @param {(value: string) => boolean} isIdentifier
2436
*/
25-
function toIdOrMemberExpressionFactory(idType, memberType) {
37+
function toIdOrMemberExpressionFactory(idType, memberType, isIdentifier) {
2638
return toIdOrMemberExpression
2739
/**
2840
* @param {Array<string|number>} ids
@@ -35,12 +47,19 @@ function toIdOrMemberExpressionFactory(idType, memberType) {
3547

3648
while (++index < ids.length) {
3749
const name = ids[index]
50+
const valid = typeof name === 'string' && isIdentifier(name)
51+
52+
// A value of `asd.123` could be turned into `asd['123']` in the JS form,
53+
// but JSX does not have a form for it, so throw.
54+
/* c8 ignore next 3 */
55+
if (idType === 'JSXIdentifier' && !valid) {
56+
throw new Error('Cannot turn `' + name + '` into a JSX identifier')
57+
}
58+
3859
/** @type {Identifier|Literal} */
3960
// @ts-expect-error: JSX is fine.
40-
const id =
41-
typeof name === 'string' && isIdentifierName(name)
42-
? {type: idType, name}
43-
: {type: 'Literal', value: name}
61+
const id = valid ? {type: idType, name} : {type: 'Literal', value: name}
62+
4463
// @ts-expect-error: JSX is fine.
4564
object = object
4665
? {
@@ -62,3 +81,29 @@ function toIdOrMemberExpressionFactory(idType, memberType) {
6281
return object
6382
}
6483
}
84+
85+
/**
86+
* Checks if the given string is a valid JSX identifier name.
87+
* @param {string} name
88+
*/
89+
function isJsxIdentifierName(name) {
90+
let index = -1
91+
92+
while (++index < name.length) {
93+
// We currently receive valid input, but this catches bugs and is needed
94+
// when externalized.
95+
/* c8 ignore next */
96+
if (!(index ? jsxCont : esStart)(name.charCodeAt(index))) return false
97+
}
98+
99+
// `false` if `name` is empty.
100+
return index > 0
101+
}
102+
103+
/**
104+
* Checks if the given character code can continue a JSX identifier.
105+
* @param {number} code
106+
*/
107+
function jsxCont(code) {
108+
return code === 45 /* `-` */ || esCont(code)
109+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"prettier": true,
139139
"rules": {
140140
"unicorn/no-await-expression-member": "off",
141+
"unicorn/prefer-code-point": "off",
141142
"capitalized-comments": "off",
142143
"complexity": "off",
143144
"max-depth": "off",

test/core.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,26 @@ test('jsx', async (t) => {
824824
'should serialize fragments, expressions'
825825
)
826826

827+
t.equal(
828+
String(compileSync('{<a-b></a-b>}', {jsx: true})),
829+
[
830+
'/*@jsxRuntime automatic @jsxImportSource react*/',
831+
'function MDXContent(props = {}) {',
832+
' let {wrapper: MDXLayout} = props.components || ({});',
833+
' return MDXLayout ? <MDXLayout {...props}><_createMdxContent /></MDXLayout> : _createMdxContent();',
834+
' function _createMdxContent() {',
835+
' let _components = Object.assign({',
836+
' "a-b": "a-b"',
837+
' }, props.components);',
838+
' return <>{<_components.a-b></_components.a-b>}</>;',
839+
' }',
840+
'}',
841+
'export default MDXContent;',
842+
''
843+
].join('\n'),
844+
'should serialize custom elements inside expressions'
845+
)
846+
827847
t.equal(
828848
String(compileSync('Hello {props.name}', {jsx: true})),
829849
[

0 commit comments

Comments
 (0)