summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/many-garlics-lick.md14
-rw-r--r--.changeset/perfect-fans-fly.md7
-rw-r--r--packages/astro/package.json2
-rw-r--r--packages/astro/src/core/build/plugins/README.md2
-rw-r--r--packages/astro/src/core/create-vite.ts2
-rw-r--r--packages/astro/src/jsx/babel.ts326
-rw-r--r--packages/astro/src/jsx/component.ts9
-rw-r--r--packages/astro/src/jsx/index.ts2
-rw-r--r--packages/astro/src/jsx/renderer.ts8
-rw-r--r--packages/astro/src/jsx/transform-options.ts17
-rw-r--r--packages/astro/src/runtime/server/index.ts2
-rw-r--r--packages/astro/src/types/astro.ts5
-rw-r--r--packages/astro/src/vite-plugin-mdx/README.md3
-rw-r--r--packages/astro/src/vite-plugin-mdx/index.ts45
-rw-r--r--packages/astro/src/vite-plugin-mdx/tag.ts113
-rw-r--r--packages/astro/src/vite-plugin-mdx/transform-jsx.ts72
-rw-r--r--packages/astro/test/fixtures/jsx/astro.config.mjs9
-rw-r--r--packages/astro/test/units/render/jsx.test.js142
-rw-r--r--packages/integrations/mdx/package.json1
-rw-r--r--packages/integrations/mdx/src/index.ts8
-rw-r--r--packages/integrations/mdx/src/server.ts (renamed from packages/astro/src/jsx/server.ts)25
21 files changed, 42 insertions, 772 deletions
diff --git a/.changeset/many-garlics-lick.md b/.changeset/many-garlics-lick.md
new file mode 100644
index 000000000..12ac2dd7c
--- /dev/null
+++ b/.changeset/many-garlics-lick.md
@@ -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.
diff --git a/.changeset/perfect-fans-fly.md b/.changeset/perfect-fans-fly.md
new file mode 100644
index 000000000..cdecf6fb1
--- /dev/null
+++ b/.changeset/perfect-fans-fly.md
@@ -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.
diff --git a/packages/astro/package.json b/packages/astro/package.json
index e55274daf..4246d0c45 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -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"
diff --git a/packages/astro/src/core/build/plugins/README.md b/packages/astro/src/core/build/plugins/README.md
index 2949233e6..667ec4a86 100644
--- a/packages/astro/src/core/build/plugins/README.md
+++ b/packages/astro/src/core/build/plugins/README.md
@@ -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 },
),
];
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index f2931523a..d01eedb0d 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -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 }),
diff --git a/packages/astro/src/jsx/babel.ts b/packages/astro/src/jsx/babel.ts
deleted file mode 100644
index 648831481..000000000
--- a/packages/astro/src/jsx/babel.ts
+++ /dev/null
@@ -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)),
- });
- }
- },
- },
- };
-}
diff --git a/packages/astro/src/jsx/component.ts b/packages/astro/src/jsx/component.ts
deleted file mode 100644
index e0ce37ef2..000000000
--- a/packages/astro/src/jsx/component.ts
+++ /dev/null
@@ -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;
-}
diff --git a/packages/astro/src/jsx/index.ts b/packages/astro/src/jsx/index.ts
deleted file mode 100644
index 2d5904e04..000000000
--- a/packages/astro/src/jsx/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { createAstroJSXComponent } from './component.js';
-export { default as renderer } from './renderer.js';
diff --git a/packages/astro/src/jsx/renderer.ts b/packages/astro/src/jsx/renderer.ts
deleted file mode 100644
index 86f4d0187..000000000
--- a/packages/astro/src/jsx/renderer.ts
+++ /dev/null
@@ -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;
diff --git a/packages/astro/src/jsx/transform-options.ts b/packages/astro/src/jsx/transform-options.ts
deleted file mode 100644
index e7405ddc0..000000000
--- a/packages/astro/src/jsx/transform-options.ts
+++ /dev/null
@@ -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' }),
- ],
- };
-}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index e2b49c231..508ece984 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -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;
diff --git a/packages/astro/src/types/astro.ts b/packages/astro/src/types/astro.ts
index 6aa79a959..1d48536d0 100644
--- a/packages/astro/src/types/astro.ts
+++ b/packages/astro/src/types/astro.ts
@@ -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[];
}
diff --git a/packages/astro/src/vite-plugin-mdx/README.md b/packages/astro/src/vite-plugin-mdx/README.md
deleted file mode 100644
index fc962ad4e..000000000
--- a/packages/astro/src/vite-plugin-mdx/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# vite-plugin-mdx
-
-Handles transforming MDX via the `astro:jsx` renderer.
diff --git a/packages/astro/src/vite-plugin-mdx/index.ts b/packages/astro/src/vite-plugin-mdx/index.ts
deleted file mode 100644
index 1c85d9226..000000000
--- a/packages/astro/src/vite-plugin-mdx/index.ts
+++ /dev/null
@@ -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);
- },
- };
-}
diff --git a/packages/astro/src/vite-plugin-mdx/tag.ts b/packages/astro/src/vite-plugin-mdx/tag.ts
deleted file mode 100644
index a65f99806..000000000
--- a/packages/astro/src/vite-plugin-mdx/tag.ts
+++ /dev/null
@@ -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);
- }
- });
- }
- }
- },
- },
- },
-};
diff --git a/packages/astro/src/vite-plugin-mdx/transform-jsx.ts b/packages/astro/src/vite-plugin-mdx/transform-jsx.ts
deleted file mode 100644
index 31db27931..000000000
--- a/packages/astro/src/vite-plugin-mdx/transform-jsx.ts
+++ /dev/null
@@ -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;
-}
diff --git a/packages/astro/test/fixtures/jsx/astro.config.mjs b/packages/astro/test/fixtures/jsx/astro.config.mjs
index 3bcbe0d8f..661b716f5 100644
--- a/packages/astro/test/fixtures/jsx/astro.config.mjs
+++ b/packages/astro/test/fixtures/jsx/astro.config.mjs
@@ -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);
- }
- }
- },
]
})
diff --git a/packages/astro/test/units/render/jsx.test.js b/packages/astro/test/units/render/jsx.test.js
deleted file mode 100644
index ba03a6f55..000000000
--- a/packages/astro/test/units/render/jsx.test.js
+++ /dev/null
@@ -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');
- }
- });
- });
-});
diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json
index a07777b6a..61dc87170 100644
--- a/packages/integrations/mdx/package.json
+++ b/packages/integrations/mdx/package.json
@@ -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": [
diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts
index de29003ff..3ebfc5f31 100644
--- a/packages/integrations/mdx/src/index.ts
+++ b/packages/integrations/mdx/src/index.ts
@@ -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'],
diff --git a/packages/astro/src/jsx/server.ts b/packages/integrations/mdx/src/server.ts
index bb71231c5..79934eb32 100644
--- a/packages/astro/src/jsx/server.ts
+++ b/packages/integrations/mdx/src/server.ts
@@ -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;
}
}