diff --git a/package-lock.json b/package-lock.json index f1ef7b7..cb478cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@knighted/jsx", - "version": "1.12.0", + "version": "1.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@knighted/jsx", - "version": "1.12.0", + "version": "1.13.0", "license": "MIT", "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1", diff --git a/package.json b/package.json index 819f836..888adc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knighted/jsx", - "version": "1.12.0", + "version": "1.13.0", "description": "Runtime JSX tagged template that renders DOM or React trees anywhere with or without a build step.", "keywords": [ "jsx runtime", diff --git a/src/transform.ts b/src/transform.ts index f40861f..6d5383b 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -79,6 +79,7 @@ export type TransformJsxSourceResult = { diagnostics: TransformDiagnostic[] declarations?: TransformTopLevelDeclaration[] hasTopLevelJsxExpression?: boolean + topLevelJsxExpressionRange?: SourceRange | null } const createParserOptions = (sourceType: TransformSourceType) => ({ @@ -422,18 +423,34 @@ const unwrapExpressionNode = (value: unknown): unknown => { return current } -const isJsxExpressionNode = (value: unknown): boolean => { +const toJsxExpressionNode = (value: unknown): Record | null => { const unwrapped = unwrapExpressionNode(value) if (!isObjectRecord(unwrapped) || typeof unwrapped.type !== 'string') { - return false + return null + } + + if (unwrapped.type === 'JSXElement' || unwrapped.type === 'JSXFragment') { + return unwrapped } - return unwrapped.type === 'JSXElement' || unwrapped.type === 'JSXFragment' + return null +} + +type TopLevelJsxExpressionMetadata = { + hasTopLevelJsxExpression: boolean + topLevelJsxExpressionRange: SourceRange | null } -const collectTopLevelJsxExpressionMetadata = (body: unknown): boolean => { +const createEmptyTopLevelJsxExpressionMetadata = (): TopLevelJsxExpressionMetadata => ({ + hasTopLevelJsxExpression: false, + topLevelJsxExpressionRange: null, +}) + +const collectTopLevelJsxExpressionMetadata = ( + body: unknown, +): TopLevelJsxExpressionMetadata => { if (!Array.isArray(body)) { - return false + return createEmptyTopLevelJsxExpressionMetadata() } for (const statement of body) { @@ -441,12 +458,16 @@ const collectTopLevelJsxExpressionMetadata = (body: unknown): boolean => { continue } - if (isJsxExpressionNode(statement.expression)) { - return true + const jsxNode = toJsxExpressionNode(statement.expression) + if (jsxNode) { + return { + hasTopLevelJsxExpression: true, + topLevelJsxExpressionRange: toSourceRange(jsxNode), + } } } - return false + return createEmptyTopLevelJsxExpressionMetadata() } const ensureSupportedOptions = (options: InternalTransformJsxSourceOptions) => { @@ -522,9 +543,12 @@ export function transformJsxSource( const declarations = internalOptions.collectTopLevelDeclarations ? collectTopLevelDeclarationMetadata(parsed.program.body) : undefined - const hasTopLevelJsxExpression = internalOptions.collectTopLevelJsxExpression + const topLevelJsxExpressionMetadata = internalOptions.collectTopLevelJsxExpression ? collectTopLevelJsxExpressionMetadata(parsed.program.body) : undefined + const hasTopLevelJsxExpression = topLevelJsxExpressionMetadata?.hasTopLevelJsxExpression + const topLevelJsxExpressionRange = + topLevelJsxExpressionMetadata?.topLevelJsxExpressionRange if (parserDiagnostics.length) { return { @@ -534,6 +558,7 @@ export function transformJsxSource( diagnostics: parserDiagnostics, declarations, hasTopLevelJsxExpression, + topLevelJsxExpressionRange, } } @@ -553,6 +578,7 @@ export function transformJsxSource( diagnostics: parserDiagnostics, declarations, hasTopLevelJsxExpression, + topLevelJsxExpressionRange, } } @@ -569,6 +595,7 @@ export function transformJsxSource( diagnostics: parserDiagnostics, declarations, hasTopLevelJsxExpression, + topLevelJsxExpressionRange, } } @@ -593,6 +620,7 @@ export function transformJsxSource( diagnostics, declarations, hasTopLevelJsxExpression, + topLevelJsxExpressionRange, } } @@ -605,5 +633,6 @@ export function transformJsxSource( diagnostics, declarations, hasTopLevelJsxExpression, + topLevelJsxExpressionRange, } } diff --git a/test/transform.test.ts b/test/transform.test.ts index 102dc05..3da50db 100644 --- a/test/transform.test.ts +++ b/test/transform.test.ts @@ -56,7 +56,7 @@ const App = () => ( }) it('collects top-level JSX expression metadata when requested', () => { - const input = '; // trailing' + const input = '() as any; // trailing' const result = transformJsxSource(input, { collectTopLevelJsxExpression: true, @@ -64,6 +64,9 @@ const App = () => ( expect(result.diagnostics).toEqual([]) expect(result.hasTopLevelJsxExpression).toBe(true) + expect(result.topLevelJsxExpressionRange).toHaveLength(2) + const [start, end] = result.topLevelJsxExpressionRange as [number, number] + expect(input.slice(start, end)).toBe('') }) it('reports false top-level JSX expression metadata when absent', () => { @@ -75,6 +78,7 @@ const App = () => ( expect(result.diagnostics).toEqual([]) expect(result.hasTopLevelJsxExpression).toBe(false) + expect(result.topLevelJsxExpressionRange).toBeNull() }) it('collects top-level declarations when requested', () => { @@ -260,6 +264,7 @@ function lowerFn() { return null } expect(result.diagnostics[0]?.source).toBe('parser') expect(typeof result.hasTopLevelJsxExpression).toBe('boolean') expect(result.hasTopLevelJsxExpression).toBe(false) + expect(result.topLevelJsxExpressionRange).toBeNull() }) it('returns parser diagnostics with source ranges', () => { @@ -684,6 +689,7 @@ const value = (input satisfies string) }) expect(result.hasTopLevelJsxExpression).toBe(false) + expect(result.topLevelJsxExpressionRange).toBeNull() vi.doUnmock('oxc-parser') vi.resetModules() @@ -721,6 +727,44 @@ const value = (input satisfies string) }) expect(result.hasTopLevelJsxExpression).toBe(true) + expect(result.topLevelJsxExpressionRange).toBeNull() + + vi.doUnmock('oxc-parser') + vi.resetModules() + }) + + it('returns false for non-JSX expression statement shapes', async () => { + vi.resetModules() + vi.doMock('oxc-parser', () => ({ + parseSync: () => ({ + errors: [], + program: { + body: [ + { + type: 'ExpressionStatement', + expression: null, + }, + { + type: 'ExpressionStatement', + expression: { + type: 'Identifier', + name: 'value', + }, + }, + ], + }, + }), + })) + + const { transformJsxSource: mockedTransformJsxSource } = + await import('../src/transform.js') + + const result = mockedTransformJsxSource('const value = 1', { + collectTopLevelJsxExpression: true, + }) + + expect(result.hasTopLevelJsxExpression).toBe(false) + expect(result.topLevelJsxExpressionRange).toBeNull() vi.doUnmock('oxc-parser') vi.resetModules()