Skip to content

Commit 6ef54c9

Browse files
feat: detect top-level jsx expressions. (#92)
1 parent 4b41dd3 commit 6ef54c9

4 files changed

Lines changed: 228 additions & 49 deletions

File tree

package-lock.json

Lines changed: 50 additions & 47 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/jsx",
3-
"version": "1.11.0",
3+
"version": "1.12.0",
44
"description": "Runtime JSX tagged template that renders DOM or React trees anywhere with or without a build step.",
55
"keywords": [
66
"jsx runtime",
@@ -173,7 +173,7 @@
173173
"jsdom": "^27.2.0",
174174
"lint-staged": "^16.2.7",
175175
"lit": "^3.2.1",
176-
"next": "^16.1.6",
176+
"next": "^16.1.7",
177177
"oxlint": "^1.51.0",
178178
"prettier": "^3.7.3",
179179
"react": "^19.0.0",

src/transform.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export type TransformTopLevelDeclaration = {
6464

6565
export type TransformJsxSourceOptions = TranspileJsxSourceOptions & {
6666
collectTopLevelDeclarations?: boolean
67+
collectTopLevelJsxExpression?: boolean
6768
}
6869

6970
type InternalTransformJsxSourceOptions = TransformJsxSourceOptions & {
@@ -77,6 +78,7 @@ export type TransformJsxSourceResult = {
7778
imports: TransformImport[]
7879
diagnostics: TransformDiagnostic[]
7980
declarations?: TransformTopLevelDeclaration[]
81+
hasTopLevelJsxExpression?: boolean
8082
}
8183

8284
const createParserOptions = (sourceType: TransformSourceType) => ({
@@ -394,6 +396,59 @@ const collectTopLevelDeclarationMetadata = (
394396
return declarations
395397
}
396398

399+
const unwrapExpressionNode = (value: unknown): unknown => {
400+
let current = value
401+
402+
while (isObjectRecord(current) && typeof current.type === 'string') {
403+
if (current.type === 'ParenthesizedExpression') {
404+
current = current.expression
405+
continue
406+
}
407+
408+
if (
409+
current.type === 'TSAsExpression' ||
410+
current.type === 'TSSatisfiesExpression' ||
411+
current.type === 'TSInstantiationExpression' ||
412+
current.type === 'TSNonNullExpression' ||
413+
current.type === 'TSTypeAssertion'
414+
) {
415+
current = current.expression
416+
continue
417+
}
418+
419+
break
420+
}
421+
422+
return current
423+
}
424+
425+
const isJsxExpressionNode = (value: unknown): boolean => {
426+
const unwrapped = unwrapExpressionNode(value)
427+
if (!isObjectRecord(unwrapped) || typeof unwrapped.type !== 'string') {
428+
return false
429+
}
430+
431+
return unwrapped.type === 'JSXElement' || unwrapped.type === 'JSXFragment'
432+
}
433+
434+
const collectTopLevelJsxExpressionMetadata = (body: unknown): boolean => {
435+
if (!Array.isArray(body)) {
436+
return false
437+
}
438+
439+
for (const statement of body) {
440+
if (!isObjectRecord(statement) || statement.type !== 'ExpressionStatement') {
441+
continue
442+
}
443+
444+
if (isJsxExpressionNode(statement.expression)) {
445+
return true
446+
}
447+
}
448+
449+
return false
450+
}
451+
397452
const ensureSupportedOptions = (options: InternalTransformJsxSourceOptions) => {
398453
if (
399454
options.sourceType !== undefined &&
@@ -433,6 +488,15 @@ const ensureSupportedOptions = (options: InternalTransformJsxSourceOptions) => {
433488
`[jsx] Unsupported collectTopLevelDeclarations value "${String(options.collectTopLevelDeclarations)}". Use true or false.`,
434489
)
435490
}
491+
492+
if (
493+
options.collectTopLevelJsxExpression !== undefined &&
494+
typeof options.collectTopLevelJsxExpression !== 'boolean'
495+
) {
496+
throw new Error(
497+
`[jsx] Unsupported collectTopLevelJsxExpression value "${String(options.collectTopLevelJsxExpression)}". Use true or false.`,
498+
)
499+
}
436500
}
437501

438502
export function transformJsxSource(
@@ -458,6 +522,9 @@ export function transformJsxSource(
458522
const declarations = internalOptions.collectTopLevelDeclarations
459523
? collectTopLevelDeclarationMetadata(parsed.program.body)
460524
: undefined
525+
const hasTopLevelJsxExpression = internalOptions.collectTopLevelJsxExpression
526+
? collectTopLevelJsxExpressionMetadata(parsed.program.body)
527+
: undefined
461528

462529
if (parserDiagnostics.length) {
463530
return {
@@ -466,6 +533,7 @@ export function transformJsxSource(
466533
imports,
467534
diagnostics: parserDiagnostics,
468535
declarations,
536+
hasTopLevelJsxExpression,
469537
}
470538
}
471539

@@ -484,6 +552,7 @@ export function transformJsxSource(
484552
imports,
485553
diagnostics: parserDiagnostics,
486554
declarations,
555+
hasTopLevelJsxExpression,
487556
}
488557
}
489558

@@ -499,6 +568,7 @@ export function transformJsxSource(
499568
imports,
500569
diagnostics: parserDiagnostics,
501570
declarations,
571+
hasTopLevelJsxExpression,
502572
}
503573
}
504574

@@ -522,6 +592,7 @@ export function transformJsxSource(
522592
imports,
523593
diagnostics,
524594
declarations,
595+
hasTopLevelJsxExpression,
525596
}
526597
}
527598

@@ -533,5 +604,6 @@ export function transformJsxSource(
533604
imports,
534605
diagnostics,
535606
declarations,
607+
hasTopLevelJsxExpression,
536608
}
537609
}

0 commit comments

Comments
 (0)