diff --git a/.changeset/many-garlics-lick.md b/.changeset/many-garlics-lick.md new file mode 100644 index 000000000000..12ac2dd7c38b --- /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 000000000000..cdecf6fb1948 --- /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 263d9389e9a4..d351523bfe5d 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 2949233e6633..667ec4a86340 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 f2931523a6ed..d01eedb0d771 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 648831481acd..000000000000 --- 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 e0ce37ef24ce..000000000000 --- 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 2d5904e04404..000000000000 --- 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 86f4d018743a..000000000000 --- 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 e7405ddc0c2d..000000000000 --- 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 { - // @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 e2b49c2311fb..508ece9847c1 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 6aa79a959c13..1d48536d07a1 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 fc962ad4ee3d..000000000000 --- 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 1c85d9226718..000000000000 --- 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 a65f99806a65..000000000000 --- 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 31db2793124f..000000000000 --- 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 { - 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 | undefined; - -/** - * Get the `jsxTransformOptions` with caching - */ -async function getJsxTransformOptions(): Promise { - 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 3bcbe0d8f796..661b716f53dd 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 ba03a6f55d22..000000000000 --- 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`
${renderSlot(result, slots['myslot'])}
`; - }); - - 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('

works

'), true); - }); - - it('Can render slots with a dash in the name', async () => { - const Wrapper = createComponent((result, _props, slots = {}) => { - return render`
${renderSlot(result, slots['my-slot'])}
`; - }); - - 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( - '

works

works

', - ), - true, - ); - }); - - it('Errors in JSX components are raised', async () => { - const Component = createAstroJSXComponent(() => { - throw new Error('uh oh'); - }); - - const Page = createComponent((result) => { - return render`
${renderComponent(result, 'Component', Component, {})}
`; - }); - - 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 773b3bf6215e..258134264646 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 de29003ff76d..3ebfc5f31f9f 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 = {}): 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 similarity index 72% rename from packages/astro/src/jsx/server.ts rename to packages/integrations/mdx/src/server.ts index bb71231c5813..79934eb3229a 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; } }