diff options
Diffstat (limited to 'packages/astro/src/jsx/babel.ts')
-rw-r--r-- | packages/astro/src/jsx/babel.ts | 136 |
1 files changed, 107 insertions, 29 deletions
diff --git a/packages/astro/src/jsx/babel.ts b/packages/astro/src/jsx/babel.ts index 0e529115d..6ce1dcf5c 100644 --- a/packages/astro/src/jsx/babel.ts +++ b/packages/astro/src/jsx/babel.ts @@ -1,5 +1,8 @@ -import type { PluginObj } from '@babel/core'; +import type { PluginMetadata } from '../vite-plugin-astro/types'; +import type { PluginObj, NodePath } from '@babel/core'; import * as t from '@babel/types'; +import { pathToFileURL } from 'node:url' +import { ClientOnlyPlaceholder } from '../runtime/server/index.js'; function isComponent(tagName: string) { return ( @@ -18,6 +21,15 @@ function hasClientDirective(node: t.JSXElement) { return false; } +function isClientOnlyComponent(node: t.JSXElement) { + for (const attr of node.openingElement.attributes) { + if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXNamespacedName') { + return jsxAttributeToString(attr) === 'client:only'; + } + } + return false; +} + function getTagName(tag: t.JSXElement) { const jsxName = tag.openingElement.name; return jsxElementNameToString(jsxName); @@ -40,28 +52,55 @@ function jsxAttributeToString(attr: t.JSXAttribute): string { return `${attr.name.name}`; } -function addClientMetadata(node: t.JSXElement, meta: { path: string; name: string }) { +function addClientMetadata(node: t.JSXElement, meta: { resolvedPath: string; path: string; name: string }) { const existingAttributes = node.openingElement.attributes.map((attr) => t.isJSXAttribute(attr) ? jsxAttributeToString(attr) : null ); if (!existingAttributes.find((attr) => attr === 'client:component-path')) { const componentPath = t.jsxAttribute( t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('component-path')), - !meta.path.startsWith('.') - ? t.stringLiteral(meta.path) - : t.jsxExpressionContainer( - t.binaryExpression( - '+', - t.stringLiteral('/@fs'), - t.memberExpression( - t.newExpression(t.identifier('URL'), [ - t.stringLiteral(meta.path), - t.identifier('import.meta.url'), - ]), - t.identifier('pathname') - ) - ) - ) + t.stringLiteral(meta.resolvedPath) + ); + node.openingElement.attributes.push(componentPath); + } + if (!existingAttributes.find((attr) => attr === 'client:component-export')) { + if (meta.name === '*') { + meta.name = getTagName(node).split('.').at(1)!; + } + const componentExport = t.jsxAttribute( + t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('component-export')), + t.stringLiteral(meta.name) + ); + node.openingElement.attributes.push(componentExport); + } + if (!existingAttributes.find((attr) => attr === 'client:component-hydration')) { + const staticMarker = t.jsxAttribute( + t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('component-hydration')) + ); + node.openingElement.attributes.push(staticMarker); + } +} + +function addClientOnlyMetadata(node: t.JSXElement, meta: { resolvedPath: string; path: string; name: string }) { + const tagName = getTagName(node); + node.openingElement = t.jsxOpeningElement(t.jsxIdentifier(ClientOnlyPlaceholder), node.openingElement.attributes) + if (node.closingElement) { + node.closingElement = t.jsxClosingElement(t.jsxIdentifier(ClientOnlyPlaceholder)) + } + const existingAttributes = node.openingElement.attributes.map((attr) => + t.isJSXAttribute(attr) ? jsxAttributeToString(attr) : null + ); + if (!existingAttributes.find((attr) => attr === 'client:display-name')) { + const displayName = t.jsxAttribute( + t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('display-name')), + t.stringLiteral(tagName) + ); + node.openingElement.attributes.push(displayName); + } + if (!existingAttributes.find((attr) => attr === 'client:component-path')) { + const componentPath = t.jsxAttribute( + t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('component-path')), + t.stringLiteral(meta.resolvedPath) ); node.openingElement.attributes.push(componentPath); } @@ -86,15 +125,24 @@ function addClientMetadata(node: t.JSXElement, meta: { path: string; name: strin export default function astroJSX(): PluginObj { return { visitor: { - Program(path) { - path.node.body.splice( - 0, - 0, - t.importDeclaration( - [t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment'))], - t.stringLiteral('astro/jsx-runtime') - ) - ); + Program: { + enter(path, state) { + if (!(state.file.metadata as PluginMetadata).astro) { + (state.file.metadata as PluginMetadata).astro = { + clientOnlyComponents: [], + hydratedComponents: [], + scripts: [], + } + } + path.node.body.splice( + 0, + 0, + t.importDeclaration( + [t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment'))], + t.stringLiteral('astro/jsx-runtime') + ) + ); + } }, ImportDeclaration(path, state) { const source = path.node.source.value; @@ -127,9 +175,11 @@ export default function astroJSX(): PluginObj { const tagName = getTagName(parentNode); if (!isComponent(tagName)) return; if (!hasClientDirective(parentNode)) return; + const isClientOnly = isClientOnlyComponent(parentNode); + if (tagName === ClientOnlyPlaceholder) return; const imports = state.get('imports') ?? new Map(); - const namespace = getTagName(parentNode).split('.'); + const namespace = tagName.split('.'); for (const [source, specs] of imports) { for (const { imported, local } of specs) { const reference = path.referencesImport(source, imported); @@ -143,10 +193,38 @@ export default function astroJSX(): PluginObj { } } } - // TODO: map unmatched identifiers back to imports if possible + const meta = path.getData('import'); if (meta) { - addClientMetadata(parentNode, meta); + let resolvedPath: string; + if (meta.path.startsWith('.')) { + const fileURL = pathToFileURL(state.filename!); + resolvedPath = `/@fs${new URL(meta.path, fileURL).pathname}`; + if (resolvedPath.endsWith('.jsx')) { + resolvedPath = resolvedPath.slice(0, -4); + } + } else { + resolvedPath = meta.path; + } + if (isClientOnly) { + (state.file.metadata as PluginMetadata).astro.clientOnlyComponents.push({ + exportName: meta.name, + specifier: meta.name, + resolvedPath + }) + + meta.resolvedPath = resolvedPath; + addClientOnlyMetadata(parentNode, meta); + } else { + (state.file.metadata as PluginMetadata).astro.hydratedComponents.push({ + exportName: meta.name, + specifier: meta.name, + resolvedPath + }) + + meta.resolvedPath = resolvedPath; + addClientMetadata(parentNode, meta); + } } else { throw new Error( `Unable to match <${getTagName( |