Add *private* built-in JSX renderer (#3697)
* feat: add private `addPageExtensions` hook * feat: experimental JSX support * chore: remove experimental.jsx option from config * chore: remove automatic astro JSX runtime detection * fix: throw warning when client:* directive is used but no client entrypoint is found * feat: add slot support to renderer * chore: remove client entrypoint from jsx renderer * test: add barebones JSX test * test: add frameworks-in-jsx test * feat: improve error message when no matching import is found * feat: support slots * fix: do not strip `astro-slot` when using JSX renderer * fix: handle null values in isVNode * fix: do not transform slots for elements Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
parent
67b5aa4ca4
commit
908c2638cb
22 changed files with 685 additions and 10 deletions
|
@ -27,6 +27,8 @@
|
|||
".": "./astro.js",
|
||||
"./env": "./env.d.ts",
|
||||
"./astro-jsx": "./astro-jsx.d.ts",
|
||||
"./jsx/*": "./dist/jsx/*",
|
||||
"./jsx-runtime": "./dist/jsx-runtime/index.js",
|
||||
"./config": "./config.mjs",
|
||||
"./internal": "./internal.js",
|
||||
"./app": "./dist/core/app/index.js",
|
||||
|
@ -87,6 +89,7 @@
|
|||
"@babel/core": "^7.18.2",
|
||||
"@babel/generator": "^7.18.2",
|
||||
"@babel/parser": "^7.18.4",
|
||||
"@babel/plugin-transform-react-jsx": "^7.17.12",
|
||||
"@babel/traverse": "^7.18.2",
|
||||
"@proload/core": "^0.3.2",
|
||||
"@proload/plugin-tsm": "^0.2.1",
|
||||
|
|
|
@ -51,7 +51,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
|||
vite: {},
|
||||
experimental: {
|
||||
ssr: false,
|
||||
integrations: false,
|
||||
integrations: false
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -346,6 +346,15 @@ export async function validateConfig(
|
|||
adapter: undefined,
|
||||
},
|
||||
};
|
||||
if (
|
||||
// TODO: expose @astrojs/mdx package
|
||||
result.integrations.find(integration => integration.name === '@astrojs/mdx')
|
||||
) {
|
||||
// Enable default JSX integration
|
||||
const { default: jsxRenderer } = await import('../jsx/renderer.js');
|
||||
(result._ctx.renderers as any[]).push(jsxRenderer);
|
||||
}
|
||||
|
||||
// Final-Pass Validation (perform checks that require the full config object)
|
||||
if (
|
||||
!result.experimental?.integrations &&
|
||||
|
|
76
packages/astro/src/jsx-runtime/index.ts
Normal file
76
packages/astro/src/jsx-runtime/index.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { Fragment, markHTMLString } from '../runtime/server/index.js';
|
||||
|
||||
const AstroJSX = Symbol('@astrojs/jsx');
|
||||
const Empty = Symbol('empty');
|
||||
|
||||
interface AstroVNode {
|
||||
[AstroJSX]: boolean;
|
||||
type: string|((...args: any) => any)|typeof Fragment;
|
||||
props: Record<string, any>;
|
||||
}
|
||||
|
||||
const toSlotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
|
||||
|
||||
export function isVNode(vnode: any): vnode is AstroVNode {
|
||||
return vnode && typeof vnode === 'object' && vnode[AstroJSX];
|
||||
}
|
||||
|
||||
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);
|
||||
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));
|
||||
return child;
|
||||
}
|
||||
|
||||
function transformSetDirectives(vnode: AstroVNode) {
|
||||
if (!('set:html' in vnode.props || 'set:text' in vnode.props)) return;
|
||||
if ('set:html' in vnode.props) {
|
||||
const children = markRawChildren(vnode.props['set:html']);
|
||||
delete vnode.props['set:html'];
|
||||
Object.assign(vnode.props, { children });
|
||||
return;
|
||||
}
|
||||
if ('set:text' in vnode.props) {
|
||||
const children = vnode.props['set:text'];
|
||||
delete vnode.props['set:text'];
|
||||
Object.assign(vnode.props, { children });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function createVNode(type: any, props: Record<string, any>) {
|
||||
const vnode: AstroVNode = {
|
||||
[AstroJSX]: true,
|
||||
type,
|
||||
props: props ?? {},
|
||||
};
|
||||
transformSetDirectives(vnode);
|
||||
transformSlots(vnode);
|
||||
return vnode;
|
||||
}
|
||||
|
||||
export {
|
||||
AstroJSX,
|
||||
createVNode as jsx,
|
||||
createVNode as jsxs,
|
||||
createVNode as jsxDEV,
|
||||
Fragment
|
||||
}
|
131
packages/astro/src/jsx/babel.ts
Normal file
131
packages/astro/src/jsx/babel.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
import * as t from "@babel/types";
|
||||
import type { PluginObj } from '@babel/core';
|
||||
|
||||
function isComponent(tagName: string) {
|
||||
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 false;
|
||||
}
|
||||
|
||||
function getTagName(tag: t.JSXElement) {
|
||||
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}`;
|
||||
}
|
||||
|
||||
function jsxAttributeToString(attr: t.JSXAttribute): string {
|
||||
if (t.isJSXNamespacedName(attr.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')) {
|
||||
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')))),
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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'))));
|
||||
},
|
||||
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 };
|
||||
return { local: spec.local.name, imported: spec.imported.value };
|
||||
});
|
||||
const imports = state.get('imports') ?? new Map();
|
||||
for (const spec of specs) {
|
||||
if (imports.has(source)) {
|
||||
const existing = imports.get(source);
|
||||
existing.add(spec);
|
||||
imports.set(source, existing)
|
||||
} else {
|
||||
imports.set(source, new Set([spec]))
|
||||
}
|
||||
}
|
||||
state.set('imports', imports);
|
||||
},
|
||||
JSXIdentifier(path, state) {
|
||||
const isAttr = path.findParent(n => t.isJSXAttribute(n));
|
||||
if (isAttr) return;
|
||||
const parent = path.findParent(n => t.isJSXElement(n))!;
|
||||
const parentNode = parent.node as t.JSXElement;
|
||||
const tagName = getTagName(parentNode);
|
||||
if (!isComponent(tagName)) return;
|
||||
if (!hasClientDirective(parentNode)) return;
|
||||
|
||||
const imports = state.get('imports') ?? new Map();
|
||||
const namespace = getTagName(parentNode).split('.');
|
||||
for (const [source, specs] of imports) {
|
||||
for (const { imported, local } of specs) {
|
||||
const reference = path.referencesImport(source, imported);
|
||||
if (reference) {
|
||||
path.setData('import', { name: imported, path: source });
|
||||
break;
|
||||
}
|
||||
if (namespace.at(0) === local) {
|
||||
path.setData('import', { name: imported, path: source });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: map unmatched identifiers back to imports if possible
|
||||
const meta = path.getData('import');
|
||||
if (meta) {
|
||||
addClientMetadata(parentNode, meta)
|
||||
} else {
|
||||
throw new Error(`Unable to match <${getTagName(parentNode)}> with client:* directive to an import statement!`);
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
};
|
15
packages/astro/src/jsx/renderer.ts
Normal file
15
packages/astro/src/jsx/renderer.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
const renderer = {
|
||||
name: 'astro:jsx',
|
||||
serverEntrypoint: 'astro/jsx/server.js',
|
||||
jsxImportSource: 'astro',
|
||||
jsxTransformOptions: async () => {
|
||||
// @ts-ignore
|
||||
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' })],
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
export default renderer;
|
37
packages/astro/src/jsx/server.ts
Normal file
37
packages/astro/src/jsx/server.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { renderJSX } from '../runtime/server/jsx.js';
|
||||
import { AstroJSX, jsx } from '../jsx-runtime/index.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 } = {}) {
|
||||
if (typeof Component !== 'function') return false;
|
||||
const slots: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(slotted)) {
|
||||
const name = slotName(key);
|
||||
slots[name] = value;
|
||||
}
|
||||
try {
|
||||
const result = await Component({ ...props, ...slots, children });
|
||||
return result[AstroJSX];
|
||||
} catch (e) {};
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
slots[name] = value;
|
||||
}
|
||||
|
||||
const { result } = this;
|
||||
try {
|
||||
const html = await renderJSX(result, jsx(Component, { ...props, ...slots, children }));
|
||||
return { html };
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export default {
|
||||
check,
|
||||
renderToStaticMarkup,
|
||||
};
|
|
@ -22,11 +22,11 @@ import { serializeProps } from './serialize.js';
|
|||
import { shorthash } from './shorthash.js';
|
||||
import { serializeListValue } from './util.js';
|
||||
|
||||
export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
|
||||
export { markHTMLString, markHTMLString as unescapeHTML, HTMLString, escapeHTML } from './escape.js';
|
||||
export type { Metadata } from './metadata';
|
||||
export { createMetadata } from './metadata.js';
|
||||
|
||||
const voidElementNames =
|
||||
export const voidElementNames =
|
||||
/^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
|
||||
const htmlBooleanAttributes =
|
||||
/^(allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i;
|
||||
|
@ -233,7 +233,7 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
|
|||
let error;
|
||||
for (const r of renderers) {
|
||||
try {
|
||||
if (await r.ssr.check(Component, props, children)) {
|
||||
if (await r.ssr.check.call({ result }, Component, props, children)) {
|
||||
renderer = r;
|
||||
break;
|
||||
}
|
||||
|
@ -299,7 +299,7 @@ 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(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,10 +318,14 @@ 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(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}!`);
|
||||
}
|
||||
|
||||
// This is a custom element without a renderer. Because of that, render it
|
||||
// as a string and the user is responsible for adding a script tag for the component definition.
|
||||
if (!html && typeof Component === 'string') {
|
||||
|
@ -340,7 +344,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
|||
}
|
||||
|
||||
if (!hydration) {
|
||||
if (isPage) {
|
||||
if (isPage || renderer?.name === 'astro:jsx') {
|
||||
return html;
|
||||
}
|
||||
return markHTMLString(html.replace(/\<\/?astro-slot\>/g, ''));
|
||||
|
@ -496,7 +500,7 @@ function internalSpreadAttributes(values: Record<any, any>, shouldEscape = true)
|
|||
// Adds support for `<Component {...value} />
|
||||
export function spreadAttributes(
|
||||
values: Record<any, any>,
|
||||
name: string,
|
||||
name?: string,
|
||||
{ class: scopedClassName }: { class?: string } = {}
|
||||
) {
|
||||
let output = '';
|
||||
|
|
63
packages/astro/src/runtime/server/jsx.ts
Normal file
63
packages/astro/src/runtime/server/jsx.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { HTMLString, markHTMLString, escapeHTML, Fragment, renderComponent, spreadAttributes, voidElementNames } from './index.js';
|
||||
import { AstroJSX, isVNode } from '../../jsx-runtime/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(''));
|
||||
}
|
||||
if (vnode[AstroJSX]) {
|
||||
if (!vnode.type && vnode.type !== 0) return '';
|
||||
if (typeof vnode.type === 'string') {
|
||||
return await renderElement(result, vnode.type, vnode.props ?? {});
|
||||
}
|
||||
if (!!vnode.type) {
|
||||
try {
|
||||
// TODO: silence Invalid hook call warning from React
|
||||
const output = await vnode.type(vnode.props ?? {});
|
||||
if (output && output[AstroJSX]) {
|
||||
return await renderJSX(result, output);
|
||||
} else if (!output) {
|
||||
return await renderJSX(result, output);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const { children = null, ...props } = vnode.props ?? {};
|
||||
const slots: Record<string, any> = {
|
||||
default: []
|
||||
}
|
||||
function extractSlots(child: any): any {
|
||||
if (Array.isArray(child)) {
|
||||
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]
|
||||
delete child.props.slot;
|
||||
return;
|
||||
}
|
||||
slots.default.push(child);
|
||||
}
|
||||
extractSlots(children);
|
||||
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));
|
||||
}
|
||||
}
|
||||
// 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(
|
||||
(children == null || children == '') && voidElementNames.test(tag)
|
||||
? `/>`
|
||||
: `>${children == null ? '' : await renderJSX(result, children)}</${tag}>`
|
||||
)}`);
|
||||
}
|
|
@ -17,6 +17,7 @@ const IMPORT_STATEMENTS: Record<string, string> = {
|
|||
react: "import React from 'react'",
|
||||
preact: "import { h } from 'preact'",
|
||||
'solid-js': "import 'solid-js/web'",
|
||||
astro: "import 'astro/jsx-runtime'",
|
||||
};
|
||||
|
||||
// A code snippet to inject into JS files to prevent esbuild reference bugs.
|
||||
|
@ -167,9 +168,9 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
|
|||
if (!importSource) {
|
||||
const multiline = code.match(/\/\*\*[\S\s]*\*\//gm) || [];
|
||||
for (const comment of multiline) {
|
||||
const [_, lib] = comment.match(/@jsxImportSource\s*(\S+)/) || [];
|
||||
const [_, lib] = comment.slice(0, -2).match(/@jsxImportSource\s*(\S+)/) || [];
|
||||
if (lib) {
|
||||
importSource = lib;
|
||||
importSource = lib.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
25
packages/astro/test/fixtures/jsx/astro.config.mjs
vendored
Normal file
25
packages/astro/test/fixtures/jsx/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import renderer from 'astro/jsx/renderer.js';
|
||||
import preact from '@astrojs/preact';
|
||||
import react from '@astrojs/react';
|
||||
import svelte from '@astrojs/svelte';
|
||||
import vue from '@astrojs/vue';
|
||||
import solid from '@astrojs/solid-js';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
{
|
||||
name: '@astrojs/test-jsx',
|
||||
hooks: {
|
||||
'astro:config:setup': ({ addRenderer }) => {
|
||||
addRenderer(renderer);
|
||||
}
|
||||
}
|
||||
},
|
||||
preact(),
|
||||
react(),
|
||||
svelte(),
|
||||
vue(),
|
||||
solid(),
|
||||
]
|
||||
})
|
21
packages/astro/test/fixtures/jsx/package.json
vendored
Normal file
21
packages/astro/test/fixtures/jsx/package.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@test/jsx",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@astrojs/preact": "workspace:*",
|
||||
"@astrojs/react": "workspace:*",
|
||||
"@astrojs/solid-js": "workspace:*",
|
||||
"@astrojs/svelte": "workspace:*",
|
||||
"@astrojs/vue": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"preact": "^10.7.3",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"solid-js": "^1.4.3",
|
||||
"svelte": "^3.48.0",
|
||||
"vue": "^3.2.36"
|
||||
}
|
||||
}
|
28
packages/astro/test/fixtures/jsx/src/components/Frameworks.jsx
vendored
Normal file
28
packages/astro/test/fixtures/jsx/src/components/Frameworks.jsx
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
import 'astro/jsx-runtime';
|
||||
import { Test } from "./Test";
|
||||
|
||||
import PreactCounter from "./PreactCounter";
|
||||
import ReactCounter from "./ReactCounter";
|
||||
import SolidCounter from "./SolidCounter";
|
||||
import SvelteCounter from "./SvelteCounter.svelte";
|
||||
import VueCounter from "./VueCounter.vue";
|
||||
|
||||
export function Preact() {
|
||||
return <Test case="has-preact"><PreactCounter /></Test>
|
||||
}
|
||||
|
||||
export function React() {
|
||||
return <Test case="has-react"><ReactCounter /></Test>
|
||||
}
|
||||
|
||||
export function Solid() {
|
||||
return <Test case="has-solid"><SolidCounter /></Test>
|
||||
}
|
||||
|
||||
export function Svelte() {
|
||||
return <Test case="has-svelte"><SvelteCounter /></Test>
|
||||
}
|
||||
|
||||
export function Vue() {
|
||||
return <Test case="has-vue"><VueCounter /></Test>
|
||||
}
|
20
packages/astro/test/fixtures/jsx/src/components/PreactCounter.tsx
vendored
Normal file
20
packages/astro/test/fixtures/jsx/src/components/PreactCounter.tsx
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { h, Fragment } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
/** a counter written in Preact */
|
||||
export default function PreactCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
const add = () => setCount((i) => i + 1);
|
||||
const subtract = () => setCount((i) => i - 1);
|
||||
|
||||
return (
|
||||
<div id="preact">
|
||||
<div className="counter">
|
||||
<button onClick={subtract}>-</button>
|
||||
<pre>{count}</pre>
|
||||
<button onClick={add}>+</button>
|
||||
</div>
|
||||
<div className="children">Preact</div>
|
||||
</div>
|
||||
);
|
||||
}
|
19
packages/astro/test/fixtures/jsx/src/components/ReactCounter.jsx
vendored
Normal file
19
packages/astro/test/fixtures/jsx/src/components/ReactCounter.jsx
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
/** a counter written in React */
|
||||
export default function ReactCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
const add = () => setCount((i) => i + 1);
|
||||
const subtract = () => setCount((i) => i - 1);
|
||||
|
||||
return (
|
||||
<div id="react">
|
||||
<div className="counter">
|
||||
<button onClick={subtract}>-</button>
|
||||
<pre>{count}</pre>
|
||||
<button onClick={add}>+</button>
|
||||
</div>
|
||||
<div className="children">React</div>
|
||||
</div>
|
||||
);
|
||||
}
|
19
packages/astro/test/fixtures/jsx/src/components/SolidCounter.jsx
vendored
Normal file
19
packages/astro/test/fixtures/jsx/src/components/SolidCounter.jsx
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { createSignal } from 'solid-js';
|
||||
|
||||
/** a counter written with Solid */
|
||||
export default function SolidCounter() {
|
||||
const [count, setCount] = createSignal(0);
|
||||
const add = () => setCount(count() + 1);
|
||||
const subtract = () => setCount(count() - 1);
|
||||
|
||||
return (
|
||||
<div id="solid">
|
||||
<div class="counter">
|
||||
<button onClick={subtract}>-</button>
|
||||
<pre>{count()}</pre>
|
||||
<button onClick={add}>+</button>
|
||||
</div>
|
||||
<div class="children">Solid</div>
|
||||
</div>
|
||||
);
|
||||
}
|
21
packages/astro/test/fixtures/jsx/src/components/SvelteCounter.svelte
vendored
Normal file
21
packages/astro/test/fixtures/jsx/src/components/SvelteCounter.svelte
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
<script>
|
||||
let count = 0;
|
||||
|
||||
function add() {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
function subtract() {
|
||||
count -= 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="svelte">
|
||||
<div class="counter">
|
||||
<button on:click={subtract}>-</button>
|
||||
<pre>{count}</pre>
|
||||
<button on:click={add}>+</button>
|
||||
</div>
|
||||
<div class="children">Svelte</div>
|
||||
</div>
|
5
packages/astro/test/fixtures/jsx/src/components/Test.jsx
vendored
Normal file
5
packages/astro/test/fixtures/jsx/src/components/Test.jsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
import 'astro';
|
||||
|
||||
export function Test({ case: id, ...slots }) {
|
||||
return <div id={id}>{Object.values(slots)}</div>
|
||||
}
|
27
packages/astro/test/fixtures/jsx/src/components/VueCounter.vue
vendored
Normal file
27
packages/astro/test/fixtures/jsx/src/components/VueCounter.vue
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div id="vue">
|
||||
<div class="counter">
|
||||
<button @click="subtract()">-</button>
|
||||
<pre>{{ count }}</pre>
|
||||
<button @click="add()">+</button>
|
||||
</div>
|
||||
<div class="children">Vue</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
export default {
|
||||
setup() {
|
||||
const count = ref(0);
|
||||
const add = () => (count.value = count.value + 1);
|
||||
const subtract = () => (count.value = count.value - 1);
|
||||
|
||||
return {
|
||||
count,
|
||||
add,
|
||||
subtract,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
6
packages/astro/test/fixtures/jsx/src/pages/component.astro
vendored
Normal file
6
packages/astro/test/fixtures/jsx/src/pages/component.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
import { Test } from '../components/Test'
|
||||
---
|
||||
|
||||
<Test case="basic">Basic</Test>
|
||||
<Test case="named"><Fragment slot="named">Named</Fragment></Test>
|
13
packages/astro/test/fixtures/jsx/src/pages/frameworks.astro
vendored
Normal file
13
packages/astro/test/fixtures/jsx/src/pages/frameworks.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
import * as Framework from '../components/Frameworks'
|
||||
---
|
||||
|
||||
<Framework.Preact />
|
||||
|
||||
<Framework.React />
|
||||
|
||||
<Framework.Solid />
|
||||
|
||||
<Framework.Svelte />
|
||||
|
||||
<Framework.Vue />
|
62
packages/astro/test/jsx.test.js
Normal file
62
packages/astro/test/jsx.test.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('jsx-runtime', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/jsx/'
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Can load simple JSX components', async () => {
|
||||
const html = await fixture.readFile('/component/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#basic').text()).to.equal('Basic');
|
||||
expect($('#named').text()).to.equal('Named');
|
||||
});
|
||||
|
||||
it('Can load Preact component inside Astro JSX', async () => {
|
||||
const html = await fixture.readFile('/frameworks/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#has-preact #preact').length).to.equal(1);
|
||||
expect($('#preact').text()).to.include('Preact');
|
||||
});
|
||||
|
||||
it('Can load React component inside Astro JSX', async () => {
|
||||
const html = await fixture.readFile('/frameworks/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#has-react #react').length).to.equal(1);
|
||||
expect($('#react').text()).to.include('React');
|
||||
});
|
||||
|
||||
it('Can load Solid component inside Astro JSX', async () => {
|
||||
const html = await fixture.readFile('/frameworks/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#has-solid #solid').length).to.equal(1);
|
||||
expect($('#solid').text()).to.include('Solid');
|
||||
});
|
||||
|
||||
it('Can load Svelte component inside Astro JSX', async () => {
|
||||
const html = await fixture.readFile('/frameworks/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#has-svelte #svelte').length).to.equal(1);
|
||||
expect($('#svelte').text()).to.include('Svelte');
|
||||
});
|
||||
|
||||
it('Can load Vue component inside Astro JSX', async () => {
|
||||
const html = await fixture.readFile('/frameworks/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#has-vue #vue').length).to.equal(1);
|
||||
expect($('#vue').text()).to.include('Vue');
|
||||
});
|
||||
});
|
|
@ -472,6 +472,7 @@ importers:
|
|||
'@babel/core': ^7.18.2
|
||||
'@babel/generator': ^7.18.2
|
||||
'@babel/parser': ^7.18.4
|
||||
'@babel/plugin-transform-react-jsx': ^7.17.12
|
||||
'@babel/traverse': ^7.18.2
|
||||
'@babel/types': ^7.18.4
|
||||
'@playwright/test': ^1.22.2
|
||||
|
@ -555,6 +556,7 @@ importers:
|
|||
'@babel/core': 7.18.2
|
||||
'@babel/generator': 7.18.2
|
||||
'@babel/parser': 7.18.4
|
||||
'@babel/plugin-transform-react-jsx': 7.17.12_@babel+core@7.18.2
|
||||
'@babel/traverse': 7.18.2
|
||||
'@proload/core': 0.3.2
|
||||
'@proload/plugin-tsm': 0.2.1_@proload+core@0.3.2
|
||||
|
@ -1467,6 +1469,35 @@ importers:
|
|||
dependencies:
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/jsx:
|
||||
specifiers:
|
||||
'@astrojs/preact': workspace:*
|
||||
'@astrojs/react': workspace:*
|
||||
'@astrojs/solid-js': workspace:*
|
||||
'@astrojs/svelte': workspace:*
|
||||
'@astrojs/vue': workspace:*
|
||||
astro: workspace:*
|
||||
preact: ^10.7.3
|
||||
react: ^18.1.0
|
||||
react-dom: ^18.1.0
|
||||
solid-js: ^1.4.3
|
||||
svelte: ^3.48.0
|
||||
vue: ^3.2.36
|
||||
dependencies:
|
||||
preact: 10.7.3
|
||||
react: 18.1.0
|
||||
react-dom: 18.1.0_react@18.1.0
|
||||
solid-js: 1.4.3
|
||||
svelte: 3.48.0
|
||||
vue: 3.2.37
|
||||
devDependencies:
|
||||
'@astrojs/preact': link:../../../../integrations/preact
|
||||
'@astrojs/react': link:../../../../integrations/react
|
||||
'@astrojs/solid-js': link:../../../../integrations/solid
|
||||
'@astrojs/svelte': link:../../../../integrations/svelte
|
||||
'@astrojs/vue': link:../../../../integrations/vue
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/legacy-build:
|
||||
specifiers:
|
||||
'@astrojs/vue': workspace:*
|
||||
|
@ -3131,6 +3162,19 @@ packages:
|
|||
'@babel/helper-plugin-utils': 7.17.12
|
||||
dev: false
|
||||
|
||||
/@babel/plugin-syntax-jsx/7.17.12_@babel+core@7.18.2:
|
||||
resolution: {integrity: sha512-spyY3E3AURfxh/RHtjx5j6hs8am5NbUBGfcZ2vB3uShSpZdQyXSf5rR5Mk76vbtlAZOelyVQ71Fg0x9SG4fsog==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.18.2
|
||||
'@babel/helper-plugin-utils': 7.17.12
|
||||
dev: false
|
||||
|
||||
/@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.18.2:
|
||||
resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
|
||||
peerDependencies:
|
||||
|
@ -3583,6 +3627,23 @@ packages:
|
|||
'@babel/types': 7.18.4
|
||||
dev: false
|
||||
|
||||
/@babel/plugin-transform-react-jsx/7.17.12_@babel+core@7.18.2:
|
||||
resolution: {integrity: sha512-Lcaw8bxd1DKht3thfD4A12dqo1X16he1Lm8rIv8sTwjAYNInRS1qHa9aJoqvzpscItXvftKDCfaEQzwoVyXpEQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.18.2
|
||||
'@babel/helper-annotate-as-pure': 7.16.7
|
||||
'@babel/helper-module-imports': 7.16.7
|
||||
'@babel/helper-plugin-utils': 7.17.12
|
||||
'@babel/plugin-syntax-jsx': 7.17.12_@babel+core@7.18.2
|
||||
'@babel/types': 7.18.4
|
||||
dev: false
|
||||
|
||||
/@babel/plugin-transform-regenerator/7.18.0_@babel+core@7.18.2:
|
||||
resolution: {integrity: sha512-C8YdRw9uzx25HSIzwA7EM7YP0FhCe5wNvJbZzjVNHHPGVcDJ3Aie+qGYYdS1oVQgn+B3eAIJbWFLrJ4Jipv7nw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -8485,6 +8546,11 @@ packages:
|
|||
|
||||
/debug/3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: false
|
||||
|
@ -11366,6 +11432,8 @@ packages:
|
|||
debug: 3.2.7
|
||||
iconv-lite: 0.4.24
|
||||
sax: 1.2.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/netmask/2.0.2:
|
||||
|
@ -11449,6 +11517,8 @@ packages:
|
|||
rimraf: 2.7.1
|
||||
semver: 5.7.1
|
||||
tar: 4.4.19
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/node-releases/2.0.5:
|
||||
|
|
Loading…
Reference in a new issue