aboutsummaryrefslogtreecommitdiff
path: root/packages/astro/src/jsx/babel.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/src/jsx/babel.ts')
-rw-r--r--packages/astro/src/jsx/babel.ts136
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(