summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/astro/src/core/config.ts4
-rw-r--r--packages/astro/src/jsx-runtime/index.ts40
-rw-r--r--packages/astro/src/jsx/babel.ts113
-rw-r--r--packages/astro/src/jsx/renderer.ts11
-rw-r--r--packages/astro/src/jsx/server.ts17
-rw-r--r--packages/astro/src/runtime/server/index.ts27
-rw-r--r--packages/astro/src/runtime/server/jsx.ts51
-rw-r--r--packages/astro/test/jsx.test.js2
8 files changed, 173 insertions, 92 deletions
diff --git a/packages/astro/src/core/config.ts b/packages/astro/src/core/config.ts
index f4e097ff6..4ad099e1f 100644
--- a/packages/astro/src/core/config.ts
+++ b/packages/astro/src/core/config.ts
@@ -51,7 +51,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
vite: {},
experimental: {
ssr: false,
- integrations: false
+ integrations: false,
},
};
@@ -348,7 +348,7 @@ export async function validateConfig(
};
if (
// TODO: expose @astrojs/mdx package
- result.integrations.find(integration => integration.name === '@astrojs/mdx')
+ result.integrations.find((integration) => integration.name === '@astrojs/mdx')
) {
// Enable default JSX integration
const { default: jsxRenderer } = await import('../jsx/renderer.js');
diff --git a/packages/astro/src/jsx-runtime/index.ts b/packages/astro/src/jsx-runtime/index.ts
index ee8660742..37d79a673 100644
--- a/packages/astro/src/jsx-runtime/index.ts
+++ b/packages/astro/src/jsx-runtime/index.ts
@@ -5,7 +5,7 @@ const Empty = Symbol('empty');
interface AstroVNode {
[AstroJSX]: boolean;
- type: string|((...args: any) => any)|typeof Fragment;
+ type: string | ((...args: any) => any) | typeof Fragment;
props: Record<string, any>;
}
@@ -19,24 +19,26 @@ export function transformSlots(vnode: AstroVNode) {
if (typeof vnode.type === 'string') return vnode;
if (!Array.isArray(vnode.props.children)) return;
const slots: Record<string, any> = {};
- vnode.props.children = vnode.props.children.map(child => {
- if (!isVNode(child)) return child;
- if (!('slot' in child.props)) return child;
- const name = toSlotName(child.props.slot)
- if (Array.isArray(slots[name])) {
- slots[name].push(child);
- } else {
- slots[name] = [child];
- }
- delete child.props.slot;
- return Empty;
- }).filter(v => v !== Empty);
+ vnode.props.children = vnode.props.children
+ .map((child) => {
+ if (!isVNode(child)) return child;
+ if (!('slot' in child.props)) return child;
+ const name = toSlotName(child.props.slot);
+ if (Array.isArray(slots[name])) {
+ slots[name].push(child);
+ } else {
+ slots[name] = [child];
+ }
+ delete child.props.slot;
+ return Empty;
+ })
+ .filter((v) => v !== Empty);
Object.assign(vnode.props, slots);
}
function markRawChildren(child: any): any {
if (typeof child === 'string') return markHTMLString(child);
- if (Array.isArray(child)) return child.map(c => markRawChildren(c));
+ if (Array.isArray(child)) return child.map((c) => markRawChildren(c));
return child;
}
@@ -57,7 +59,7 @@ function transformSetDirectives(vnode: AstroVNode) {
}
function createVNode(type: any, props: Record<string, any>) {
- const vnode: AstroVNode = {
+ const vnode: AstroVNode = {
[AstroJSX]: true,
type,
props: props ?? {},
@@ -67,10 +69,4 @@ function createVNode(type: any, props: Record<string, any>) {
return vnode;
}
-export {
- AstroJSX,
- createVNode as jsx,
- createVNode as jsxs,
- createVNode as jsxDEV,
- Fragment
-}
+export { AstroJSX, createVNode as jsx, createVNode as jsxs, createVNode as jsxDEV, Fragment };
diff --git a/packages/astro/src/jsx/babel.ts b/packages/astro/src/jsx/babel.ts
index 33bd8652b..0e529115d 100644
--- a/packages/astro/src/jsx/babel.ts
+++ b/packages/astro/src/jsx/babel.ts
@@ -1,85 +1,110 @@
-import * as t from "@babel/types";
import type { PluginObj } from '@babel/core';
+import * as t from '@babel/types';
function isComponent(tagName: string) {
- return (
- (tagName[0] && tagName[0].toLowerCase() !== tagName[0]) ||
- tagName.includes(".") ||
- /[^a-zA-Z]/.test(tagName[0])
- );
+ return (
+ (tagName[0] && tagName[0].toLowerCase() !== tagName[0]) ||
+ tagName.includes('.') ||
+ /[^a-zA-Z]/.test(tagName[0])
+ );
}
function hasClientDirective(node: t.JSXElement) {
for (const attr of node.openingElement.attributes) {
if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXNamespacedName') {
- return attr.name.namespace.name === 'client'
+ return attr.name.namespace.name === 'client';
}
}
return false;
}
function getTagName(tag: t.JSXElement) {
- const jsxName = tag.openingElement.name;
- return jsxElementNameToString(jsxName);
+ const jsxName = tag.openingElement.name;
+ return jsxElementNameToString(jsxName);
}
function jsxElementNameToString(node: t.JSXOpeningElement['name']): string {
- if (t.isJSXMemberExpression(node)) {
- return `${jsxElementNameToString(node.object)}.${node.property.name}`;
- }
- if (t.isJSXIdentifier(node) || t.isIdentifier(node)) {
- return node.name;
- }
- return `${node.namespace.name}:${node.name.name}`;
+ if (t.isJSXMemberExpression(node)) {
+ return `${jsxElementNameToString(node.object)}.${node.property.name}`;
+ }
+ if (t.isJSXIdentifier(node) || t.isIdentifier(node)) {
+ return node.name;
+ }
+ return `${node.namespace.name}:${node.name.name}`;
}
function jsxAttributeToString(attr: t.JSXAttribute): string {
if (t.isJSXNamespacedName(attr.name)) {
- return `${attr.name.namespace.name}:${attr.name.name.name}`
+ return `${attr.name.namespace.name}:${attr.name.name.name}`;
}
return `${attr.name.name}`;
}
-function addClientMetadata(node: t.JSXElement, meta: { 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')) {
+function addClientMetadata(node: t.JSXElement, meta: { 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')))),
+ !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')
+ )
+ )
+ )
);
node.openingElement.attributes.push(componentPath);
}
- if (!existingAttributes.find(attr => attr === 'client:component-export')) {
+ 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),
+ t.stringLiteral(meta.name)
);
node.openingElement.attributes.push(componentExport);
}
- if (!existingAttributes.find(attr => attr === 'client:component-hydration')) {
+ if (!existingAttributes.find((attr) => attr === 'client:component-hydration')) {
const staticMarker = t.jsxAttribute(
- t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('component-hydration')),
- )
+ t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('component-hydration'))
+ );
node.openingElement.attributes.push(staticMarker);
}
}
export default function astroJSX(): PluginObj {
- return {
- visitor: {
+ 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'))));
+ 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;
if (source.startsWith('astro/jsx-runtime')) return;
- const specs = path.node.specifiers.map(spec => {
- if (t.isImportDefaultSpecifier(spec)) return { local: spec.local.name, imported: 'default' }
- if (t.isImportNamespaceSpecifier(spec)) return { local: spec.local.name, imported: '*' }
- if (t.isIdentifier(spec.imported)) return { local: spec.local.name, imported: spec.imported.name };
+ const specs = path.node.specifiers.map((spec) => {
+ if (t.isImportDefaultSpecifier(spec))
+ return { local: spec.local.name, imported: 'default' };
+ if (t.isImportNamespaceSpecifier(spec)) return { local: spec.local.name, imported: '*' };
+ if (t.isIdentifier(spec.imported))
+ return { local: spec.local.name, imported: spec.imported.name };
return { local: spec.local.name, imported: spec.imported.value };
});
const imports = state.get('imports') ?? new Map();
@@ -87,17 +112,17 @@ export default function astroJSX(): PluginObj {
if (imports.has(source)) {
const existing = imports.get(source);
existing.add(spec);
- imports.set(source, existing)
+ imports.set(source, existing);
} else {
- imports.set(source, new Set([spec]))
+ imports.set(source, new Set([spec]));
}
}
state.set('imports', imports);
},
JSXIdentifier(path, state) {
- const isAttr = path.findParent(n => t.isJSXAttribute(n));
+ const isAttr = path.findParent((n) => t.isJSXAttribute(n));
if (isAttr) return;
- const parent = path.findParent(n => t.isJSXElement(n))!;
+ const parent = path.findParent((n) => t.isJSXElement(n))!;
const parentNode = parent.node as t.JSXElement;
const tagName = getTagName(parentNode);
if (!isComponent(tagName)) return;
@@ -121,11 +146,15 @@ export default function astroJSX(): PluginObj {
// TODO: map unmatched identifiers back to imports if possible
const meta = path.getData('import');
if (meta) {
- addClientMetadata(parentNode, meta)
+ addClientMetadata(parentNode, meta);
} else {
- throw new Error(`Unable to match <${getTagName(parentNode)}> with client:* directive to an import statement!`);
+ throw new Error(
+ `Unable to match <${getTagName(
+ parentNode
+ )}> with client:* directive to an import statement!`
+ );
}
},
- }
- };
-};
+ },
+ };
+}
diff --git a/packages/astro/src/jsx/renderer.ts b/packages/astro/src/jsx/renderer.ts
index 94a63b5fe..54f4d6a3d 100644
--- a/packages/astro/src/jsx/renderer.ts
+++ b/packages/astro/src/jsx/renderer.ts
@@ -4,12 +4,17 @@ const renderer = {
jsxImportSource: 'astro',
jsxTransformOptions: async () => {
// @ts-ignore
- const { default: { default: jsx } } = await import('@babel/plugin-transform-react-jsx');
+ const {
+ default: { default: jsx },
+ } = await import('@babel/plugin-transform-react-jsx');
const { default: astroJSX } = await import('./babel.js');
return {
- plugins: [astroJSX(), jsx({}, { throwIfNamespace: false, runtime: 'automatic', importSource: 'astro' })],
+ plugins: [
+ astroJSX(),
+ jsx({}, { throwIfNamespace: false, runtime: 'automatic', importSource: 'astro' }),
+ ],
};
},
-}
+};
export default renderer;
diff --git a/packages/astro/src/jsx/server.ts b/packages/astro/src/jsx/server.ts
index c75135b90..1f7ff850c 100644
--- a/packages/astro/src/jsx/server.ts
+++ b/packages/astro/src/jsx/server.ts
@@ -1,9 +1,13 @@
-import { renderJSX } from '../runtime/server/jsx.js';
import { AstroJSX, jsx } from '../jsx-runtime/index.js';
+import { renderJSX } from '../runtime/server/jsx.js';
const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
-export async function check(Component: any, props: any, { default: children = null, ...slotted } = {}) {
+export async function check(
+ Component: any,
+ props: any,
+ { default: children = null, ...slotted } = {}
+) {
if (typeof Component !== 'function') return false;
const slots: Record<string, any> = {};
for (const [key, value] of Object.entries(slotted)) {
@@ -13,11 +17,16 @@ export async function check(Component: any, props: any, { default: children = nu
try {
const result = await Component({ ...props, ...slots, children });
return result[AstroJSX];
- } catch (e) {};
+ } catch (e) {}
return false;
}
-export async function renderToStaticMarkup(this: any, Component: any, props = {}, { default: children = null, ...slotted } = {}) {
+export async function renderToStaticMarkup(
+ this: any,
+ Component: any,
+ props = {},
+ { default: children = null, ...slotted } = {}
+) {
const slots: Record<string, any> = {};
for (const [key, value] of Object.entries(slotted)) {
const name = slotName(key);
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 33d41ef5d..e1ffd69e5 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -22,7 +22,12 @@ import { serializeProps } from './serialize.js';
import { shorthash } from './shorthash.js';
import { serializeListValue } from './util.js';
-export { markHTMLString, markHTMLString as unescapeHTML, HTMLString, escapeHTML } from './escape.js';
+export {
+ escapeHTML,
+ HTMLString,
+ markHTMLString,
+ markHTMLString as unescapeHTML,
+} from './escape.js';
export type { Metadata } from './metadata';
export { createMetadata } from './metadata.js';
@@ -299,7 +304,13 @@ Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '
// We already know that renderer.ssr.check() has failed
// but this will throw a much more descriptive error!
renderer = matchingRenderers[0];
- ({ html } = await renderer.ssr.renderToStaticMarkup.call({ result }, Component, props, children, metadata));
+ ({ html } = await renderer.ssr.renderToStaticMarkup.call(
+ { result },
+ Component,
+ props,
+ children,
+ metadata
+ ));
} else {
throw new Error(`Unable to render ${metadata.displayName}!
@@ -318,12 +329,20 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
if (metadata.hydrate === 'only') {
html = await renderSlot(result, slots?.fallback);
} else {
- ({ html } = await renderer.ssr.renderToStaticMarkup.call({ result }, Component, props, children, metadata));
+ ({ html } = await renderer.ssr.renderToStaticMarkup.call(
+ { result },
+ Component,
+ props,
+ children,
+ metadata
+ ));
}
}
if (renderer && !renderer.clientEntrypoint && metadata.hydrate) {
- throw new Error(`${metadata.displayName} component has a \`client:${metadata.hydrate}\` directive, but no client entrypoint was provided by ${renderer.name}!`);
+ throw new Error(
+ `${metadata.displayName} component has a \`client:${metadata.hydrate}\` directive, but no client entrypoint was provided by ${renderer.name}!`
+ );
}
// This is a custom element without a renderer. Because of that, render it
diff --git a/packages/astro/src/runtime/server/jsx.ts b/packages/astro/src/runtime/server/jsx.ts
index dc4df923a..015378797 100644
--- a/packages/astro/src/runtime/server/jsx.ts
+++ b/packages/astro/src/runtime/server/jsx.ts
@@ -1,13 +1,28 @@
-import { HTMLString, markHTMLString, escapeHTML, Fragment, renderComponent, spreadAttributes, voidElementNames } from './index.js';
import { AstroJSX, isVNode } from '../../jsx-runtime/index.js';
+import {
+ escapeHTML,
+ Fragment,
+ HTMLString,
+ markHTMLString,
+ renderComponent,
+ spreadAttributes,
+ voidElementNames,
+} from './index.js';
export async function renderJSX(result: any, vnode: any): Promise<any> {
switch (true) {
- case (vnode instanceof HTMLString): return vnode;
- case (typeof vnode === 'string'): return markHTMLString(escapeHTML(vnode));
- case (!vnode && vnode !== 0): return '';
- case (vnode.type === Fragment): return renderJSX(result, vnode.props.children);
- case (Array.isArray(vnode)): return markHTMLString((await Promise.all(vnode.map((v: any) => renderJSX(result, v)))).join(''));
+ case vnode instanceof HTMLString:
+ return vnode;
+ case typeof vnode === 'string':
+ return markHTMLString(escapeHTML(vnode));
+ case !vnode && vnode !== 0:
+ return '';
+ case vnode.type === Fragment:
+ return renderJSX(result, vnode.props.children);
+ case Array.isArray(vnode):
+ return markHTMLString(
+ (await Promise.all(vnode.map((v: any) => renderJSX(result, v)))).join('')
+ );
}
if (vnode[AstroJSX]) {
if (!vnode.type && vnode.type !== 0) return '';
@@ -27,17 +42,17 @@ export async function renderJSX(result: any, vnode: any): Promise<any> {
const { children = null, ...props } = vnode.props ?? {};
const slots: Record<string, any> = {
- default: []
- }
+ default: [],
+ };
function extractSlots(child: any): any {
if (Array.isArray(child)) {
- return child.map(c => extractSlots(c));
+ return child.map((c) => extractSlots(c));
}
if (!isVNode(child)) {
return slots.default.push(child);
}
if ('slot' in child.props) {
- slots[child.props.slot] = [...(slots[child.props.slot] ?? []), child]
+ slots[child.props.slot] = [...(slots[child.props.slot] ?? []), child];
delete child.props.slot;
return;
}
@@ -47,17 +62,25 @@ export async function renderJSX(result: any, vnode: any): Promise<any> {
for (const [key, value] of Object.entries(slots)) {
slots[key] = () => renderJSX(result, value);
}
- return markHTMLString(await renderComponent(result, vnode.type.name, vnode.type, props, slots));
+ return markHTMLString(
+ await renderComponent(result, vnode.type.name, vnode.type, props, slots)
+ );
}
}
// numbers, plain objects, etc
return markHTMLString(`${vnode}`);
}
-async function renderElement(result: any, tag: string, { children, ...props }: Record<string, any>) {
- return markHTMLString(`<${tag}${spreadAttributes(props)}${markHTMLString(
+async function renderElement(
+ result: any,
+ tag: string,
+ { children, ...props }: Record<string, any>
+) {
+ return markHTMLString(
+ `<${tag}${spreadAttributes(props)}${markHTMLString(
(children == null || children == '') && voidElementNames.test(tag)
? `/>`
: `>${children == null ? '' : await renderJSX(result, children)}</${tag}>`
- )}`);
+ )}`
+ );
}
diff --git a/packages/astro/test/jsx.test.js b/packages/astro/test/jsx.test.js
index bf05d35c0..41671699c 100644
--- a/packages/astro/test/jsx.test.js
+++ b/packages/astro/test/jsx.test.js
@@ -7,7 +7,7 @@ describe('jsx-runtime', () => {
before(async () => {
fixture = await loadFixture({
- root: './fixtures/jsx/'
+ root: './fixtures/jsx/',
});
await fixture.build();
});