0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-17 23:11:29 -05:00

Cleanup unused JSX code (#11741)

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Bjorn Lu 2024-08-20 17:39:51 +08:00 committed by GitHub
parent f239242d90
commit 6617491c3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 42 additions and 772 deletions

View file

@ -0,0 +1,14 @@
---
'astro': major
---
Removes internal JSX handling and moves the responsibility to the `@astrojs/mdx` package directly. The following exports are also now removed:
- `astro/jsx/babel.js`
- `astro/jsx/component.js`
- `astro/jsx/index.js`
- `astro/jsx/renderer.js`
- `astro/jsx/server.js`
- `astro/jsx/transform-options.js`
If your project includes `.mdx` files, you must upgrade `@astrojs/mdx` to the latest version so that it doesn't rely on these entrypoints to handle your JSX.

View file

@ -0,0 +1,7 @@
---
'@astrojs/mdx': minor
---
Updates adapter server entrypoint to use `@astrojs/mdx/server.js`
This is an internal change. Handling JSX in your `.mdx` files has been moved from Astro internals and is now the responsibility of this integration. You should not notice a change in your project, and no update to your code is required.

View file

@ -39,7 +39,7 @@
"./astro-jsx": "./astro-jsx.d.ts",
"./tsconfigs/*.json": "./tsconfigs/*",
"./tsconfigs/*": "./tsconfigs/*.json",
"./jsx/*": "./dist/jsx/*",
"./jsx/rehype.js": "./dist/jsx/rehype.js",
"./jsx-runtime": {
"types": "./jsx-runtime.d.ts",
"default": "./dist/jsx-runtime/index.js"

View file

@ -21,7 +21,7 @@ The emitted file has content similar to:
```js
const renderers = [
Object.assign(
{ name: 'astro:jsx', serverEntrypoint: 'astro/jsx/server.js', jsxImportSource: 'astro' },
{ name: 'astro:framework', serverEntrypoint: '@astrojs/framework/server.js' },
{ ssr: server_default },
),
];

View file

@ -28,7 +28,6 @@ import htmlVitePlugin from '../vite-plugin-html/index.js';
import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js';
import astroLoadFallbackPlugin from '../vite-plugin-load-fallback/index.js';
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
import mdxVitePlugin from '../vite-plugin-mdx/index.js';
import astroScannerPlugin from '../vite-plugin-scanner/index.js';
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
@ -136,7 +135,6 @@ export async function createVite(
astroEnv({ settings, mode, fs, sync }),
markdownVitePlugin({ settings, logger }),
htmlVitePlugin(),
mdxVitePlugin(),
astroPostprocessVitePlugin(),
astroIntegrationsContainerPlugin({ settings, logger }),
astroScriptsPageSSRPlugin({ settings }),

View file

@ -1,326 +0,0 @@
import type { PluginObj } from '@babel/core';
import * as t from '@babel/types';
import { AstroError } from '../core/errors/errors.js';
import { AstroErrorData } from '../core/errors/index.js';
import { resolvePath } from '../core/viteUtils.js';
import { createDefaultAstroMetadata } from '../vite-plugin-astro/metadata.js';
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
const ClientOnlyPlaceholder = 'astro-client-only';
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 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);
}
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: { 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')),
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('.').slice(1).join('.')!;
}
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);
}
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);
}
}
/**
* @deprecated This plugin is no longer used. Remove in Astro 5.0
*/
export default function astroJSX(): PluginObj {
return {
visitor: {
Program: {
enter(path, state) {
if (!(state.file.metadata as PluginMetadata).astro) {
(state.file.metadata as PluginMetadata).astro = createDefaultAstroMetadata();
}
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);
},
JSXMemberExpression(path, state) {
const node = path.node;
// Skip automatic `_components` in MDX files
if (
state.filename?.endsWith('.mdx') &&
t.isJSXIdentifier(node.object) &&
node.object.name === '_components'
) {
return;
}
const parent = path.findParent((n) => t.isJSXElement(n.node))!;
const parentNode = parent.node as t.JSXElement;
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 = tagName.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) {
const name = imported === '*' ? imported : tagName;
path.setData('import', { name, path: source });
break;
}
}
}
const meta = path.getData('import');
if (meta) {
const resolvedPath = resolvePath(meta.path, state.filename!);
if (isClientOnly) {
(state.file.metadata as PluginMetadata).astro.clientOnlyComponents.push({
exportName: meta.name,
localName: '',
specifier: tagName,
resolvedPath,
});
meta.resolvedPath = resolvedPath;
addClientOnlyMetadata(parentNode, meta);
} else {
(state.file.metadata as PluginMetadata).astro.hydratedComponents.push({
exportName: '*',
localName: '',
specifier: tagName,
resolvedPath,
});
meta.resolvedPath = resolvedPath;
addClientMetadata(parentNode, meta);
}
} else {
throw new Error(
`Unable to match <${getTagName(
parentNode,
)}> with client:* directive to an import statement!`,
);
}
},
JSXIdentifier(path, state) {
const isAttr = path.findParent((n) => t.isJSXAttribute(n.node));
if (isAttr) return;
const parent = path.findParent((n) => t.isJSXElement(n.node))!;
const parentNode = parent.node as t.JSXElement;
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 = tagName.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;
}
}
}
const meta = path.getData('import');
if (meta) {
// If JSX is importing an Astro component, e.g. using MDX for templating,
// check Astro node's props and make sure they are valid for an Astro component
if (meta.path.endsWith('.astro')) {
const displayName = getTagName(parentNode);
for (const attr of parentNode.openingElement.attributes) {
if (t.isJSXAttribute(attr)) {
const name = jsxAttributeToString(attr);
if (name.startsWith('client:')) {
// eslint-disable-next-line
console.warn(
`You are attempting to render <${displayName} ${name} />, but ${displayName} is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.`,
);
}
}
}
}
const resolvedPath = resolvePath(meta.path, state.filename!);
if (isClientOnly) {
(state.file.metadata as PluginMetadata).astro.clientOnlyComponents.push({
exportName: meta.name,
localName: '',
specifier: meta.name,
resolvedPath,
});
meta.resolvedPath = resolvedPath;
addClientOnlyMetadata(parentNode, meta);
} else {
(state.file.metadata as PluginMetadata).astro.hydratedComponents.push({
exportName: meta.name,
localName: '',
specifier: meta.name,
resolvedPath,
});
meta.resolvedPath = resolvedPath;
addClientMetadata(parentNode, meta);
}
} else {
throw new AstroError({
...AstroErrorData.NoMatchingImport,
message: AstroErrorData.NoMatchingImport.message(getTagName(parentNode)),
});
}
},
},
};
}

View file

@ -1,9 +0,0 @@
import { __astro_tag_component__ } from '../runtime/server/index.js';
import renderer from './renderer.js';
const ASTRO_JSX_RENDERER_NAME = renderer.name;
export function createAstroJSXComponent(factory: (...args: any[]) => any) {
__astro_tag_component__(factory, ASTRO_JSX_RENDERER_NAME);
return factory;
}

View file

@ -1,2 +0,0 @@
export { createAstroJSXComponent } from './component.js';
export { default as renderer } from './renderer.js';

View file

@ -1,8 +0,0 @@
import type { AstroRenderer } from '../types/public/integrations.js';
const renderer: AstroRenderer = {
name: 'astro:jsx',
serverEntrypoint: 'astro/jsx/server.js',
};
export default renderer;

View file

@ -1,17 +0,0 @@
import type { JSXTransformConfig } from '../types/astro.js';
/**
* @deprecated This function is no longer used. Remove in Astro 5.0
*/
export async function jsxTransformOptions(): Promise<JSXTransformConfig> {
// @ts-expect-error types not found
const plugin = await import('@babel/plugin-transform-react-jsx');
const jsx = plugin.default?.default ?? plugin.default;
const { default: astroJSX } = await import('./babel.js');
return {
plugins: [
astroJSX(),
jsx({}, { throwIfNamespace: false, runtime: 'automatic', importSource: 'astro' }),
],
};
}

View file

@ -57,7 +57,7 @@ export function mergeSlots(...slotted: unknown[]) {
return slots;
}
/** @internal Associate JSX components with a specific renderer (see /src/vite-plugin-jsx/tag.ts) */
/** @internal Associate JSX components with a specific renderer (see /packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts) */
export function __astro_tag_component__(Component: unknown, rendererName: string) {
if (!Component) return;
if (typeof Component !== 'function') return;

View file

@ -78,11 +78,6 @@ export interface ComponentInstance {
getStaticPaths?: (options: GetStaticPathsOptions) => GetStaticPathsResult;
}
export type JSXTransformConfig = Pick<
babel.TransformOptions,
'presets' | 'plugins' | 'inputSourceMap'
>;
export interface ManifestData {
routes: RouteData[];
}

View file

@ -1,3 +0,0 @@
# vite-plugin-mdx
Handles transforming MDX via the `astro:jsx` renderer.

View file

@ -1,45 +0,0 @@
import { type Plugin, transformWithEsbuild } from 'vite';
import { CONTENT_FLAG, PROPAGATED_ASSET_FLAG } from '../content/index.js';
import { astroEntryPrefix } from '../core/build/plugins/plugin-component-entry.js';
import { removeQueryString } from '../core/path.js';
import { transformJSX } from './transform-jsx.js';
// Format inspired by https://github.com/vitejs/vite/blob/main/packages/vite/src/node/constants.ts#L54
const SPECIAL_QUERY_REGEX = new RegExp(
`[?&](?:worker|sharedworker|raw|url|${CONTENT_FLAG}|${PROPAGATED_ASSET_FLAG})\\b`,
);
/**
* @deprecated This plugin is no longer used. Remove in Astro 5.0
*/
export default function mdxVitePlugin(): Plugin {
return {
name: 'astro:jsx',
enforce: 'pre', // run transforms before other plugins
async transform(code, id, opts) {
// Skip special queries and astro entries. We skip astro entries here as we know it doesn't contain
// JSX code, and also because we can't detect the import source to apply JSX transforms.
if (SPECIAL_QUERY_REGEX.test(id) || id.startsWith(astroEntryPrefix)) {
return null;
}
id = removeQueryString(id);
// Shortcut: only use Astro renderer for MD and MDX files
if (!id.endsWith('.mdx')) {
return null;
}
const { code: jsxCode } = await transformWithEsbuild(code, id, {
loader: 'jsx',
jsx: 'preserve',
sourcemap: 'inline',
tsconfigRaw: {
compilerOptions: {
// Ensure client:only imports are treeshaken
verbatimModuleSyntax: false,
importsNotUsedAsValues: 'remove',
},
},
});
return await transformJSX(jsxCode, id, opts?.ssr);
},
};
}

View file

@ -1,113 +0,0 @@
import type { PluginObj } from '@babel/core';
import * as t from '@babel/types';
import astroJsxRenderer from '../jsx/renderer.js';
const rendererName = astroJsxRenderer.name;
/**
* This plugin handles every file that runs through our JSX plugin.
* Since we statically match every JSX file to an Astro renderer based on import scanning,
* it would be helpful to embed some of that metadata at runtime.
*
* This plugin crawls each export in the file and "tags" each export with a given `rendererName`.
* This allows us to automatically match a component to a renderer and skip the usual `check()` calls.
*
* @deprecated This plugin is no longer used. Remove in Astro 5.0
*/
export const tagExportsPlugin: PluginObj = {
visitor: {
Program: {
// Inject `import { __astro_tag_component__ } from 'astro/runtime/server/index.js'`
enter(path) {
path.node.body.splice(
0,
0,
t.importDeclaration(
[
t.importSpecifier(
t.identifier('__astro_tag_component__'),
t.identifier('__astro_tag_component__'),
),
],
t.stringLiteral('astro/runtime/server/index.js'),
),
);
},
// For each export we found, inject `__astro_tag_component__(exportName, rendererName)`
exit(path, state) {
const exportedIds = state.get('astro:tags');
if (exportedIds) {
for (const id of exportedIds) {
path.node.body.push(
t.expressionStatement(
t.callExpression(t.identifier('__astro_tag_component__'), [
t.identifier(id),
t.stringLiteral(rendererName),
]),
),
);
}
}
},
},
ExportDeclaration: {
/**
* For default anonymous function export, we need to give them a unique name
* @param path
* @returns
*/
enter(path) {
const node = path.node;
if (!t.isExportDefaultDeclaration(node)) return;
if (t.isArrowFunctionExpression(node.declaration) || t.isCallExpression(node.declaration)) {
const varName = t.isArrowFunctionExpression(node.declaration)
? '_arrow_function'
: '_hoc_function';
const uidIdentifier = path.scope.generateUidIdentifier(varName);
path.insertBefore(
t.variableDeclaration('const', [t.variableDeclarator(uidIdentifier, node.declaration)]),
);
node.declaration = uidIdentifier;
} else if (t.isFunctionDeclaration(node.declaration) && !node.declaration.id?.name) {
const uidIdentifier = path.scope.generateUidIdentifier('_function');
node.declaration.id = uidIdentifier;
}
},
exit(path, state) {
const node = path.node;
if (node.exportKind === 'type') return;
if (t.isExportAllDeclaration(node)) return;
const addTag = (id: string) => {
const tags = state.get('astro:tags') ?? [];
state.set('astro:tags', [...tags, id]);
};
if (t.isExportNamedDeclaration(node) || t.isExportDefaultDeclaration(node)) {
if (t.isIdentifier(node.declaration)) {
addTag(node.declaration.name);
} else if (t.isFunctionDeclaration(node.declaration) && node.declaration.id?.name) {
addTag(node.declaration.id.name);
} else if (t.isVariableDeclaration(node.declaration)) {
node.declaration.declarations?.forEach((declaration) => {
if (t.isArrowFunctionExpression(declaration.init) && t.isIdentifier(declaration.id)) {
addTag(declaration.id.name);
}
});
} else if (t.isObjectExpression(node.declaration)) {
node.declaration.properties?.forEach((property) => {
if (t.isProperty(property) && t.isIdentifier(property.key)) {
addTag(property.key.name);
}
});
} else if (t.isExportNamedDeclaration(node) && !node.source) {
node.specifiers.forEach((specifier) => {
if (t.isExportSpecifier(specifier) && t.isIdentifier(specifier.exported)) {
addTag(specifier.local.name);
}
});
}
}
},
},
},
};

View file

@ -1,72 +0,0 @@
import babel from '@babel/core';
import type { TransformResult } from 'rollup';
import { jsxTransformOptions } from '../jsx/transform-options.js';
import type { JSXTransformConfig } from '../types/astro.js';
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
import { tagExportsPlugin } from './tag.js';
/**
* @deprecated This function is no longer used. Remove in Astro 5.0
*/
export async function transformJSX(
code: string,
id: string,
ssr?: boolean,
): Promise<TransformResult> {
const options = await getJsxTransformOptions();
const plugins = ssr ? [...(options.plugins ?? []), tagExportsPlugin] : options.plugins;
const result = await babel.transformAsync(code, {
presets: options.presets,
plugins,
cwd: process.cwd(),
filename: id,
ast: false,
compact: false,
sourceMaps: true,
configFile: false,
babelrc: false,
browserslistConfigFile: false,
inputSourceMap: options.inputSourceMap,
});
// TODO: Be more strict about bad return values here.
// Should we throw an error instead? Should we never return `{code: ""}`?
if (!result) return null;
const { astro } = result.metadata as unknown as PluginMetadata;
return {
code: result.code || '',
map: result.map,
meta: {
astro,
vite: {
// Setting this vite metadata to `ts` causes Vite to resolve .js
// extensions to .ts files.
lang: 'ts',
},
},
};
}
let cachedJsxTransformOptions: Promise<JSXTransformConfig> | JSXTransformConfig | undefined;
/**
* Get the `jsxTransformOptions` with caching
*/
async function getJsxTransformOptions(): Promise<JSXTransformConfig> {
if (cachedJsxTransformOptions) {
return cachedJsxTransformOptions;
}
const options = jsxTransformOptions();
// Cache the promise
cachedJsxTransformOptions = options;
// After the promise is resolved, cache the final resolved options
options.then((resolvedOptions) => {
cachedJsxTransformOptions = resolvedOptions;
});
return options;
}

View file

@ -5,7 +5,6 @@ import solid from '@astrojs/solid-js';
import svelte from '@astrojs/svelte';
import vue from '@astrojs/vue';
import { defineConfig } from 'astro/config';
import renderer from 'astro/jsx/renderer.js';
export default defineConfig({
@ -22,13 +21,5 @@ export default defineConfig({
mdx(),
svelte(),
vue(),
{
name: '@astrojs/test-jsx',
hooks: {
'astro:config:setup': ({ addRenderer }) => {
addRenderer(renderer);
}
}
},
]
})

View file

@ -1,142 +0,0 @@
import * as assert from 'node:assert/strict';
import { before, describe, it } from 'node:test';
import { RenderContext } from '../../../dist/core/render-context.js';
import { loadRenderer } from '../../../dist/core/render/index.js';
import { jsx } from '../../../dist/jsx-runtime/index.js';
import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js';
import {
createComponent,
render,
renderComponent,
renderSlot,
} from '../../../dist/runtime/server/index.js';
import { createBasicPipeline } from '../test-utils.js';
const createAstroModule = (AstroComponent) => ({ default: AstroComponent });
const loadJSXRenderer = () => loadRenderer(jsxRenderer, { import: (s) => import(s) });
// NOTE: This test may be testing an outdated JSX setup
describe('core/render', () => {
describe('Astro JSX components', () => {
let pipeline;
before(async () => {
pipeline = createBasicPipeline({
renderers: [await loadJSXRenderer()],
});
});
it('Can render slots', async () => {
const Wrapper = createComponent((result, _props, slots = {}) => {
return render`<div>${renderSlot(result, slots['myslot'])}</div>`;
});
const Page = createAstroJSXComponent(() => {
return jsx(Wrapper, {
children: [
jsx('p', {
slot: 'myslot',
className: 'n',
children: 'works',
}),
],
});
});
const mod = createAstroModule(Page);
const request = new Request('http://example.com/');
const routeData = {
type: 'page',
pathname: '/index',
component: 'src/pages/index.mdx',
params: {},
};
const renderContext = RenderContext.create({ pipeline, request, routeData });
const response = await renderContext.render(mod);
assert.equal(response.status, 200);
const html = await response.text();
assert.equal(html.includes('<div><p class="n">works</p></div>'), true);
});
it('Can render slots with a dash in the name', async () => {
const Wrapper = createComponent((result, _props, slots = {}) => {
return render`<div>${renderSlot(result, slots['my-slot'])}</div>`;
});
const Page = createAstroJSXComponent(() => {
return jsx('main', {
children: [
jsx(Wrapper, {
// Children as an array
children: [
jsx('p', {
slot: 'my-slot',
className: 'n',
children: 'works',
}),
],
}),
jsx(Wrapper, {
// Children as a VNode
children: jsx('p', {
slot: 'my-slot',
className: 'p',
children: 'works',
}),
}),
],
});
});
const mod = createAstroModule(Page);
const request = new Request('http://example.com/');
const routeData = {
type: 'page',
pathname: '/index',
component: 'src/pages/index.mdx',
params: {},
};
const renderContext = RenderContext.create({ pipeline, request, routeData });
const response = await renderContext.render(mod);
assert.equal(response.status, 200);
const html = await response.text();
assert.equal(
html.includes(
'<main><div><p class="n">works</p></div><div><p class="p">works</p></div></main>',
),
true,
);
});
it('Errors in JSX components are raised', async () => {
const Component = createAstroJSXComponent(() => {
throw new Error('uh oh');
});
const Page = createComponent((result) => {
return render`<div>${renderComponent(result, 'Component', Component, {})}</div>`;
});
const mod = createAstroModule(Page);
const request = new Request('http://example.com/');
const routeData = {
type: 'page',
pathname: '/index',
component: 'src/pages/index.mdx',
params: {},
};
const renderContext = RenderContext.create({ pipeline, request, routeData });
const response = await renderContext.render(mod);
try {
await response.text();
assert.equal(false, true, 'should not have been successful');
} catch (err) {
assert.equal(err.message, 'uh oh');
}
});
});
});

View file

@ -20,6 +20,7 @@
"homepage": "https://docs.astro.build/en/guides/integrations-guide/mdx/",
"exports": {
".": "./dist/index.js",
"./server.js": "./dist/server.js",
"./package.json": "./package.json"
},
"files": [

View file

@ -8,7 +8,6 @@ import type {
ContentEntryType,
HookParameters,
} from 'astro';
import astroJSXRenderer from 'astro/jsx/renderer.js';
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
import type { PluggableList } from 'unified';
import type { OptimizeOptions } from './rehype-optimize-static.js';
@ -37,7 +36,7 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & {
export function getContainerRenderer(): ContainerRenderer {
return {
name: 'astro:jsx',
serverEntrypoint: 'astro/jsx/server.js',
serverEntrypoint: '@astrojs/mdx/server.js',
};
}
@ -53,7 +52,10 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI
const { updateConfig, config, addPageExtension, addContentEntryType, addRenderer } =
params as SetupHookParams;
addRenderer(astroJSXRenderer);
addRenderer({
name: 'astro:jsx',
serverEntrypoint: '@astrojs/mdx/server.js',
});
addPageExtension('.mdx');
addContentEntryType({
extensions: ['.mdx'],

View file

@ -1,7 +1,7 @@
import { AstroError, AstroUserError } from '../core/errors/errors.js';
import { AstroJSX, jsx } from '../jsx-runtime/index.js';
import { renderJSX } from '../runtime/server/jsx.js';
import type { NamedSSRLoadedRendererValue } from '../types/public/internal.js';
import type { NamedSSRLoadedRendererValue } from 'astro';
import { AstroError } from 'astro/errors';
import { AstroJSX, jsx } from 'astro/jsx-runtime';
import { renderJSX } from 'astro/runtime/server/index.js';
const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
@ -53,15 +53,14 @@ function throwEnhancedErrorIfMdxComponent(error: Error, Component: any) {
// if the exception is from an mdx component
// throw an error
if (Component[Symbol.for('mdx-component')]) {
// if it's an AstroUserError, we don't need to re-throw, keep the original hint
if (AstroUserError.is(error)) return;
throw new AstroError({
message: error.message,
title: error.name,
hint: `This issue often occurs when your MDX component encounters runtime errors.`,
name: error.name,
stack: error.stack,
});
// if it's an existing AstroError, we don't need to re-throw, keep the original hint
if (AstroError.is(error)) return;
// Mimic the fields of the internal `AstroError` class (not from `astro/errors`) to
// provide better title and hint for the error overlay
(error as any).title = error.name;
(error as any).hint =
`This issue often occurs when your MDX component encounters runtime errors.`;
throw error;
}
}