Skip to content

Commit

Permalink
feat: support unnamed default export (#81)
Browse files Browse the repository at this point in the history
* feat: support unnamed default export

* refactor: wrapped function does not need to be async

* refactor: output as much as possible with the function as it is.

* feat: supports various export styles
  • Loading branch information
usualoma authored Feb 22, 2024
1 parent 660382c commit 3561d31
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 26 deletions.
91 changes: 66 additions & 25 deletions src/vite/island-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
import type { Plugin } from 'vite'
import { COMPONENT_NAME, DATA_HONO_TEMPLATE, DATA_SERIALIZED_PROPS } from '../constants.js'

function addSSRCheck(funcName: string, componentName: string, isAsync = false) {
function addSSRCheck(funcName: string, componentName: string) {
const isSSR = memberExpression(
memberExpression(identifier('import'), identifier('meta')),
identifier('env.SSR')
Expand Down Expand Up @@ -98,11 +98,7 @@ function addSSRCheck(funcName: string, componentName: string, isAsync = false) {
)

const returnStmt = returnStatement(conditionalExpression(isSSR, ssrElement, clientElement))
const functionExpr = functionExpression(null, [identifier('props')], blockStatement([returnStmt]))
if (isAsync) {
functionExpr.async = true
}
return functionExpr
return functionExpression(null, [identifier('props')], blockStatement([returnStmt]))
}

export const transformJsxTags = (contents: string, componentName: string) => {
Expand All @@ -112,39 +108,84 @@ export const transformJsxTags = (contents: string, componentName: string) => {
})

if (ast) {
let wrappedFunctionId

traverse(ast, {
ExportDefaultDeclaration(path) {
if (path.node.declaration.type === 'FunctionDeclaration') {
const functionId = path.node.declaration.id
if (!functionId) {
return
ExportNamedDeclaration(path) {
for (const specifier of path.node.specifiers) {
if (specifier.type !== 'ExportSpecifier') {
continue
}
const isAsync = path.node.declaration.async
const originalFunctionId = identifier(functionId.name + 'Original')

const originalFunction = functionExpression(
null,
path.node.declaration.params,
path.node.declaration.body
)
if (isAsync) {
originalFunction.async = true
const exportAs =
specifier.exported.type === 'StringLiteral'
? specifier.exported.value
: specifier.exported.name
if (exportAs !== 'default') {
continue
}

const wrappedFunction = addSSRCheck(specifier.local.name, componentName)
const wrappedFunctionId = identifier('Wrapped' + specifier.local.name)
path.insertBefore(
variableDeclaration('const', [variableDeclarator(originalFunctionId, originalFunction)])
variableDeclaration('const', [variableDeclarator(wrappedFunctionId, wrappedFunction)])
)

const wrappedFunction = addSSRCheck(originalFunctionId.name, componentName, isAsync)
const wrappedFunctionId = identifier('Wrapped' + functionId.name)
specifier.local.name = wrappedFunctionId.name
}
},
ExportDefaultDeclaration(path) {
const declarationType = path.node.declaration.type
if (
declarationType === 'FunctionDeclaration' ||
declarationType === 'FunctionExpression' ||
declarationType === 'ArrowFunctionExpression' ||
declarationType === 'Identifier'
) {
const functionName =
(declarationType === 'Identifier'
? path.node.declaration.name
: (declarationType === 'FunctionDeclaration' ||
declarationType === 'FunctionExpression') &&
path.node.declaration.id?.name) || '__HonoIsladComponent__'

let originalFunctionId
if (declarationType === 'Identifier') {
originalFunctionId = path.node.declaration
} else {
originalFunctionId = identifier(functionName + 'Original')

const originalFunction =
path.node.declaration.type === 'FunctionExpression' ||
path.node.declaration.type === 'ArrowFunctionExpression'
? path.node.declaration
: functionExpression(
null,
path.node.declaration.params,
path.node.declaration.body,
undefined,
path.node.declaration.async
)

path.insertBefore(
variableDeclaration('const', [
variableDeclarator(originalFunctionId, originalFunction),
])
)
}

const wrappedFunction = addSSRCheck(originalFunctionId.name, componentName)
wrappedFunctionId = identifier('Wrapped' + functionName)
path.replaceWith(
variableDeclaration('const', [variableDeclarator(wrappedFunctionId, wrappedFunction)])
)
path.insertAfter(exportDefaultDeclaration(wrappedFunctionId))
}
},
})

if (wrappedFunctionId) {
ast.program.body.push(exportDefaultDeclaration(wrappedFunctionId))
}

const { code } = generate(ast)
return code
}
Expand Down
72 changes: 71 additions & 1 deletion test/unit/vite/island-components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,80 @@ export default WrappedBadge;`
`const AsyncComponentOriginal = async function () {
return <h1>Hello</h1>;
};
const WrappedAsyncComponent = async function (props) {
const WrappedAsyncComponent = function (props) {
return import.meta.env.SSR ? <honox-island component-name="AsyncComponent.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><AsyncComponentOriginal {...props}></AsyncComponentOriginal>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <AsyncComponentOriginal {...props}></AsyncComponentOriginal>;
};
export default WrappedAsyncComponent;`
)
})

it('unnamed', () => {
const code = `export default async function() {
return <h1>Hello</h1>
}`
const result = transformJsxTags(code, 'UnnamedComponent.tsx')
expect(result).toBe(
`const __HonoIsladComponent__Original = async function () {
return <h1>Hello</h1>;
};
const Wrapped__HonoIsladComponent__ = function (props) {
return import.meta.env.SSR ? <honox-island component-name="UnnamedComponent.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>;
};
export default Wrapped__HonoIsladComponent__;`
)
})

it('arrow - block', () => {
const code = `export default () => {
return <h1>Hello</h1>
}`
const result = transformJsxTags(code, 'UnnamedComponent.tsx')
expect(result).toBe(
`const __HonoIsladComponent__Original = () => {
return <h1>Hello</h1>;
};
const Wrapped__HonoIsladComponent__ = function (props) {
return import.meta.env.SSR ? <honox-island component-name="UnnamedComponent.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>;
};
export default Wrapped__HonoIsladComponent__;`
)
})

it('arrow - expression', () => {
const code = 'export default () => <h1>Hello</h1>'
const result = transformJsxTags(code, 'UnnamedComponent.tsx')
expect(result).toBe(
`const __HonoIsladComponent__Original = () => <h1>Hello</h1>;
const Wrapped__HonoIsladComponent__ = function (props) {
return import.meta.env.SSR ? <honox-island component-name="UnnamedComponent.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <__HonoIsladComponent__Original {...props}></__HonoIsladComponent__Original>;
};
export default Wrapped__HonoIsladComponent__;`
)
})

it('export via variable', () => {
const code = 'export default ExportViaVariable'
const result = transformJsxTags(code, 'ExportViaVariable.tsx')
expect(result).toBe(
`const WrappedExportViaVariable = function (props) {
return import.meta.env.SSR ? <honox-island component-name="ExportViaVariable.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><ExportViaVariable {...props}></ExportViaVariable>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <ExportViaVariable {...props}></ExportViaVariable>;
};
export default WrappedExportViaVariable;`
)
})

it('export via specifier', () => {
const code = `const utilityFn = () => {}
const ExportViaVariable = () => <h1>Hello</h1>
export { utilityFn, ExportViaVariable as default }`
const result = transformJsxTags(code, 'ExportViaVariable.tsx')
expect(result).toBe(
`const utilityFn = () => {};
const ExportViaVariable = () => <h1>Hello</h1>;
const WrappedExportViaVariable = function (props) {
return import.meta.env.SSR ? <honox-island component-name="ExportViaVariable.tsx" data-serialized-props={JSON.stringify(Object.fromEntries(Object.entries(props).filter(([key]) => key !== "children")))}><ExportViaVariable {...props}></ExportViaVariable>{props.children ? <template data-hono-template="">{props.children}</template> : null}</honox-island> : <ExportViaVariable {...props}></ExportViaVariable>;
};
export { utilityFn, WrappedExportViaVariable as default };`
)
})
})

0 comments on commit 3561d31

Please sign in to comment.