[ci] format

This commit is contained in:
natemoo-re 2022-06-24 20:13:12 +00:00 committed by github-actions[bot]
parent 908c2638cb
commit 5e716e8cd5
8 changed files with 173 additions and 92 deletions

View file

@ -51,7 +51,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
vite: {}, vite: {},
experimental: { experimental: {
ssr: false, ssr: false,
integrations: false integrations: false,
}, },
}; };
@ -348,7 +348,7 @@ export async function validateConfig(
}; };
if ( if (
// TODO: expose @astrojs/mdx package // 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 // Enable default JSX integration
const { default: jsxRenderer } = await import('../jsx/renderer.js'); const { default: jsxRenderer } = await import('../jsx/renderer.js');

View file

@ -5,7 +5,7 @@ const Empty = Symbol('empty');
interface AstroVNode { interface AstroVNode {
[AstroJSX]: boolean; [AstroJSX]: boolean;
type: string|((...args: any) => any)|typeof Fragment; type: string | ((...args: any) => any) | typeof Fragment;
props: Record<string, any>; props: Record<string, any>;
} }
@ -19,24 +19,26 @@ export function transformSlots(vnode: AstroVNode) {
if (typeof vnode.type === 'string') return vnode; if (typeof vnode.type === 'string') return vnode;
if (!Array.isArray(vnode.props.children)) return; if (!Array.isArray(vnode.props.children)) return;
const slots: Record<string, any> = {}; const slots: Record<string, any> = {};
vnode.props.children = vnode.props.children.map(child => { vnode.props.children = vnode.props.children
if (!isVNode(child)) return child; .map((child) => {
if (!('slot' in child.props)) return child; if (!isVNode(child)) return child;
const name = toSlotName(child.props.slot) if (!('slot' in child.props)) return child;
if (Array.isArray(slots[name])) { const name = toSlotName(child.props.slot);
slots[name].push(child); if (Array.isArray(slots[name])) {
} else { slots[name].push(child);
slots[name] = [child]; } else {
} slots[name] = [child];
delete child.props.slot; }
return Empty; delete child.props.slot;
}).filter(v => v !== Empty); return Empty;
})
.filter((v) => v !== Empty);
Object.assign(vnode.props, slots); Object.assign(vnode.props, slots);
} }
function markRawChildren(child: any): any { function markRawChildren(child: any): any {
if (typeof child === 'string') return markHTMLString(child); 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; return child;
} }
@ -57,7 +59,7 @@ function transformSetDirectives(vnode: AstroVNode) {
} }
function createVNode(type: any, props: Record<string, any>) { function createVNode(type: any, props: Record<string, any>) {
const vnode: AstroVNode = { const vnode: AstroVNode = {
[AstroJSX]: true, [AstroJSX]: true,
type, type,
props: props ?? {}, props: props ?? {},
@ -67,10 +69,4 @@ function createVNode(type: any, props: Record<string, any>) {
return vnode; return vnode;
} }
export { export { AstroJSX, createVNode as jsx, createVNode as jsxs, createVNode as jsxDEV, Fragment };
AstroJSX,
createVNode as jsx,
createVNode as jsxs,
createVNode as jsxDEV,
Fragment
}

View file

@ -1,85 +1,110 @@
import * as t from "@babel/types";
import type { PluginObj } from '@babel/core'; import type { PluginObj } from '@babel/core';
import * as t from '@babel/types';
function isComponent(tagName: string) { function isComponent(tagName: string) {
return ( return (
(tagName[0] && tagName[0].toLowerCase() !== tagName[0]) || (tagName[0] && tagName[0].toLowerCase() !== tagName[0]) ||
tagName.includes(".") || tagName.includes('.') ||
/[^a-zA-Z]/.test(tagName[0]) /[^a-zA-Z]/.test(tagName[0])
); );
} }
function hasClientDirective(node: t.JSXElement) { function hasClientDirective(node: t.JSXElement) {
for (const attr of node.openingElement.attributes) { for (const attr of node.openingElement.attributes) {
if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXNamespacedName') { if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXNamespacedName') {
return attr.name.namespace.name === 'client' return attr.name.namespace.name === 'client';
} }
} }
return false; return false;
} }
function getTagName(tag: t.JSXElement) { function getTagName(tag: t.JSXElement) {
const jsxName = tag.openingElement.name; const jsxName = tag.openingElement.name;
return jsxElementNameToString(jsxName); return jsxElementNameToString(jsxName);
} }
function jsxElementNameToString(node: t.JSXOpeningElement['name']): string { function jsxElementNameToString(node: t.JSXOpeningElement['name']): string {
if (t.isJSXMemberExpression(node)) { if (t.isJSXMemberExpression(node)) {
return `${jsxElementNameToString(node.object)}.${node.property.name}`; return `${jsxElementNameToString(node.object)}.${node.property.name}`;
} }
if (t.isJSXIdentifier(node) || t.isIdentifier(node)) { if (t.isJSXIdentifier(node) || t.isIdentifier(node)) {
return node.name; return node.name;
} }
return `${node.namespace.name}:${node.name.name}`; return `${node.namespace.name}:${node.name.name}`;
} }
function jsxAttributeToString(attr: t.JSXAttribute): string { function jsxAttributeToString(attr: t.JSXAttribute): string {
if (t.isJSXNamespacedName(attr.name)) { 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}`; return `${attr.name.name}`;
} }
function addClientMetadata(node: t.JSXElement, meta: { path: string, name: string }) { function addClientMetadata(node: t.JSXElement, meta: { path: string; name: string }) {
const existingAttributes = node.openingElement.attributes.map(attr => t.isJSXAttribute(attr) ? jsxAttributeToString(attr) : null); const existingAttributes = node.openingElement.attributes.map((attr) =>
if (!existingAttributes.find(attr => attr === 'client:component-path')) { t.isJSXAttribute(attr) ? jsxAttributeToString(attr) : null
);
if (!existingAttributes.find((attr) => attr === 'client:component-path')) {
const componentPath = t.jsxAttribute( const componentPath = t.jsxAttribute(
t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('component-path')), 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); node.openingElement.attributes.push(componentPath);
} }
if (!existingAttributes.find(attr => attr === 'client:component-export')) { if (!existingAttributes.find((attr) => attr === 'client:component-export')) {
if (meta.name === '*') { if (meta.name === '*') {
meta.name = getTagName(node).split('.').at(1)!; meta.name = getTagName(node).split('.').at(1)!;
} }
const componentExport = t.jsxAttribute( const componentExport = t.jsxAttribute(
t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('component-export')), t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier('component-export')),
t.stringLiteral(meta.name), t.stringLiteral(meta.name)
); );
node.openingElement.attributes.push(componentExport); 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( 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); node.openingElement.attributes.push(staticMarker);
} }
} }
export default function astroJSX(): PluginObj { export default function astroJSX(): PluginObj {
return { return {
visitor: { visitor: {
Program(path) { 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) { ImportDeclaration(path, state) {
const source = path.node.source.value; const source = path.node.source.value;
if (source.startsWith('astro/jsx-runtime')) return; if (source.startsWith('astro/jsx-runtime')) return;
const specs = path.node.specifiers.map(spec => { const specs = path.node.specifiers.map((spec) => {
if (t.isImportDefaultSpecifier(spec)) return { local: spec.local.name, imported: 'default' } if (t.isImportDefaultSpecifier(spec))
if (t.isImportNamespaceSpecifier(spec)) return { local: spec.local.name, imported: '*' } return { local: spec.local.name, imported: 'default' };
if (t.isIdentifier(spec.imported)) return { local: spec.local.name, imported: spec.imported.name }; 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 }; return { local: spec.local.name, imported: spec.imported.value };
}); });
const imports = state.get('imports') ?? new Map(); const imports = state.get('imports') ?? new Map();
@ -87,17 +112,17 @@ export default function astroJSX(): PluginObj {
if (imports.has(source)) { if (imports.has(source)) {
const existing = imports.get(source); const existing = imports.get(source);
existing.add(spec); existing.add(spec);
imports.set(source, existing) imports.set(source, existing);
} else { } else {
imports.set(source, new Set([spec])) imports.set(source, new Set([spec]));
} }
} }
state.set('imports', imports); state.set('imports', imports);
}, },
JSXIdentifier(path, state) { JSXIdentifier(path, state) {
const isAttr = path.findParent(n => t.isJSXAttribute(n)); const isAttr = path.findParent((n) => t.isJSXAttribute(n));
if (isAttr) return; 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 parentNode = parent.node as t.JSXElement;
const tagName = getTagName(parentNode); const tagName = getTagName(parentNode);
if (!isComponent(tagName)) return; if (!isComponent(tagName)) return;
@ -121,11 +146,15 @@ export default function astroJSX(): PluginObj {
// TODO: map unmatched identifiers back to imports if possible // TODO: map unmatched identifiers back to imports if possible
const meta = path.getData('import'); const meta = path.getData('import');
if (meta) { if (meta) {
addClientMetadata(parentNode, meta) addClientMetadata(parentNode, meta);
} else { } 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!`
);
} }
}, },
} },
}; };
}; }

View file

@ -4,12 +4,17 @@ const renderer = {
jsxImportSource: 'astro', jsxImportSource: 'astro',
jsxTransformOptions: async () => { jsxTransformOptions: async () => {
// @ts-ignore // @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'); const { default: astroJSX } = await import('./babel.js');
return { return {
plugins: [astroJSX(), jsx({}, { throwIfNamespace: false, runtime: 'automatic', importSource: 'astro' })], plugins: [
astroJSX(),
jsx({}, { throwIfNamespace: false, runtime: 'automatic', importSource: 'astro' }),
],
}; };
}, },
} };
export default renderer; export default renderer;

View file

@ -1,9 +1,13 @@
import { renderJSX } from '../runtime/server/jsx.js';
import { AstroJSX, jsx } from '../jsx-runtime/index.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()); 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; if (typeof Component !== 'function') return false;
const slots: Record<string, any> = {}; const slots: Record<string, any> = {};
for (const [key, value] of Object.entries(slotted)) { for (const [key, value] of Object.entries(slotted)) {
@ -13,11 +17,16 @@ export async function check(Component: any, props: any, { default: children = nu
try { try {
const result = await Component({ ...props, ...slots, children }); const result = await Component({ ...props, ...slots, children });
return result[AstroJSX]; return result[AstroJSX];
} catch (e) {}; } catch (e) {}
return false; 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> = {}; const slots: Record<string, any> = {};
for (const [key, value] of Object.entries(slotted)) { for (const [key, value] of Object.entries(slotted)) {
const name = slotName(key); const name = slotName(key);

View file

@ -22,7 +22,12 @@ import { serializeProps } from './serialize.js';
import { shorthash } from './shorthash.js'; import { shorthash } from './shorthash.js';
import { serializeListValue } from './util.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 type { Metadata } from './metadata';
export { createMetadata } from './metadata.js'; 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 // We already know that renderer.ssr.check() has failed
// but this will throw a much more descriptive error! // but this will throw a much more descriptive error!
renderer = matchingRenderers[0]; 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 { } else {
throw new Error(`Unable to render ${metadata.displayName}! 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') { if (metadata.hydrate === 'only') {
html = await renderSlot(result, slots?.fallback); html = await renderSlot(result, slots?.fallback);
} else { } 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) { 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 // This is a custom element without a renderer. Because of that, render it

View file

@ -1,13 +1,28 @@
import { HTMLString, markHTMLString, escapeHTML, Fragment, renderComponent, spreadAttributes, voidElementNames } from './index.js';
import { AstroJSX, isVNode } from '../../jsx-runtime/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> { export async function renderJSX(result: any, vnode: any): Promise<any> {
switch (true) { switch (true) {
case (vnode instanceof HTMLString): return vnode; case vnode instanceof HTMLString:
case (typeof vnode === 'string'): return markHTMLString(escapeHTML(vnode)); return vnode;
case (!vnode && vnode !== 0): return ''; case typeof vnode === 'string':
case (vnode.type === Fragment): return renderJSX(result, vnode.props.children); return markHTMLString(escapeHTML(vnode));
case (Array.isArray(vnode)): return markHTMLString((await Promise.all(vnode.map((v: any) => renderJSX(result, v)))).join('')); 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[AstroJSX]) {
if (!vnode.type && vnode.type !== 0) return ''; 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 { children = null, ...props } = vnode.props ?? {};
const slots: Record<string, any> = { const slots: Record<string, any> = {
default: [] default: [],
} };
function extractSlots(child: any): any { function extractSlots(child: any): any {
if (Array.isArray(child)) { if (Array.isArray(child)) {
return child.map(c => extractSlots(c)); return child.map((c) => extractSlots(c));
} }
if (!isVNode(child)) { if (!isVNode(child)) {
return slots.default.push(child); return slots.default.push(child);
} }
if ('slot' in child.props) { 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; delete child.props.slot;
return; return;
} }
@ -47,17 +62,25 @@ export async function renderJSX(result: any, vnode: any): Promise<any> {
for (const [key, value] of Object.entries(slots)) { for (const [key, value] of Object.entries(slots)) {
slots[key] = () => renderJSX(result, value); 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 // numbers, plain objects, etc
return markHTMLString(`${vnode}`); return markHTMLString(`${vnode}`);
} }
async function renderElement(result: any, tag: string, { children, ...props }: Record<string, any>) { async function renderElement(
return markHTMLString(`<${tag}${spreadAttributes(props)}${markHTMLString( result: any,
tag: string,
{ children, ...props }: Record<string, any>
) {
return markHTMLString(
`<${tag}${spreadAttributes(props)}${markHTMLString(
(children == null || children == '') && voidElementNames.test(tag) (children == null || children == '') && voidElementNames.test(tag)
? `/>` ? `/>`
: `>${children == null ? '' : await renderJSX(result, children)}</${tag}>` : `>${children == null ? '' : await renderJSX(result, children)}</${tag}>`
)}`); )}`
);
} }

View file

@ -7,7 +7,7 @@ describe('jsx-runtime', () => {
before(async () => { before(async () => {
fixture = await loadFixture({ fixture = await loadFixture({
root: './fixtures/jsx/' root: './fixtures/jsx/',
}); });
await fixture.build(); await fixture.build();
}); });