diff --git a/.changeset/four-pants-juggle.md b/.changeset/four-pants-juggle.md new file mode 100644 index 000000000000..e10cb5019395 --- /dev/null +++ b/.changeset/four-pants-juggle.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Marks renderer `jsxImportSource` and `jsxTransformOptions` options as deprecated as they are no longer used since Astro 3.0 diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 010815695b20..e39689a25b75 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -2577,9 +2577,9 @@ export interface AstroRenderer { clientEntrypoint?: string; /** Import entrypoint for the server/build/ssr renderer. */ serverEntrypoint: string; - /** JSX identifier (e.g. 'react' or 'solid-js') */ + /** @deprecated Vite plugins should transform the JSX instead */ jsxImportSource?: string; - /** Babel transform options */ + /** @deprecated Vite plugins should transform the JSX instead */ jsxTransformOptions?: JSXTransformFn; } diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index 56092bd323ec..cc32fb6f165d 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -137,7 +137,7 @@ export async function createVite( envVitePlugin({ settings }), markdownVitePlugin({ settings, logger }), htmlVitePlugin(), - mdxVitePlugin({ settings, logger }), + mdxVitePlugin(), astroPostprocessVitePlugin(), astroIntegrationsContainerPlugin({ settings, logger }), astroScriptsPageSSRPlugin({ settings }), diff --git a/packages/astro/src/jsx/renderer.ts b/packages/astro/src/jsx/renderer.ts index 39d7f5adb71b..413257faab97 100644 --- a/packages/astro/src/jsx/renderer.ts +++ b/packages/astro/src/jsx/renderer.ts @@ -1,19 +1,11 @@ -const renderer = { +import type { AstroRenderer } from '../@types/astro.js'; +import { jsxTransformOptions } from './transform-options.js'; + +const renderer: AstroRenderer = { name: 'astro:jsx', serverEntrypoint: 'astro/jsx/server.js', jsxImportSource: 'astro', - jsxTransformOptions: async () => { - // @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' }), - ], - }; - }, + jsxTransformOptions, }; export default renderer; diff --git a/packages/astro/src/jsx/transform-options.ts b/packages/astro/src/jsx/transform-options.ts new file mode 100644 index 000000000000..4b51d85b8b04 --- /dev/null +++ b/packages/astro/src/jsx/transform-options.ts @@ -0,0 +1,14 @@ +import type { JSXTransformConfig } from '../@types/astro.js'; + +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/vite-plugin-mdx/README.md b/packages/astro/src/vite-plugin-mdx/README.md index 554651869496..fc962ad4ee3d 100644 --- a/packages/astro/src/vite-plugin-mdx/README.md +++ b/packages/astro/src/vite-plugin-mdx/README.md @@ -1,3 +1,3 @@ -# vite-plugin-jsx +# vite-plugin-mdx -Modifies Vite’s built-in JSX behavior to allow for React, Preact, and Solid.js to coexist and all use `.jsx` and `.tsx` extensions. +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 index 94fef27830b9..7e86aed288f4 100644 --- a/packages/astro/src/vite-plugin-mdx/index.ts +++ b/packages/astro/src/vite-plugin-mdx/index.ts @@ -1,99 +1,19 @@ -import type { TransformResult } from 'rollup'; -import { type Plugin, type ResolvedConfig, transformWithEsbuild } from 'vite'; -import type { AstroRenderer, AstroSettings } from '../@types/astro.js'; -import type { Logger } from '../core/logger/core.js'; -import type { PluginMetadata } from '../vite-plugin-astro/types.js'; - -import babel from '@babel/core'; +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 tagExportsPlugin from './tag.js'; - -interface TransformJSXOptions { - code: string; - id: string; - mode: string; - renderer: AstroRenderer; - ssr: boolean; - root: URL; -} - -async function transformJSX({ - code, - mode, - id, - ssr, - renderer, - root, -}: TransformJSXOptions): Promise { - const { jsxTransformOptions } = renderer; - const options = await jsxTransformOptions!({ mode, ssr }); - const plugins = [...(options.plugins || [])]; - if (ssr) { - plugins.push(await tagExportsPlugin({ rendererName: renderer.name, root })); - } - 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, - 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; - - if (renderer.name === 'astro:jsx') { - 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', - }, - }, - }; - } - - return { - code: result.code || '', - map: result.map, - }; -} - -interface AstroPluginJSXOptions { - settings: AstroSettings; - logger: Logger; -} +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` ); -/** Use Astro config to allow for alternate or multiple JSX renderers (by default Vite will assume React) */ -export default function mdxVitePlugin({ settings }: AstroPluginJSXOptions): Plugin { - let viteConfig: ResolvedConfig; - // A reference to Astro's internal JSX renderer. - let astroJSXRenderer: AstroRenderer; - +// TODO: Move this Vite plugin into `@astrojs/mdx` in Astro 5 +export default function mdxVitePlugin(): Plugin { return { name: 'astro:jsx', enforce: 'pre', // run transforms before other plugins - async configResolved(resolvedConfig) { - viteConfig = resolvedConfig; - astroJSXRenderer = settings.renderers.find((r) => r.jsxImportSource === 'astro')!; - }, 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. @@ -117,14 +37,7 @@ export default function mdxVitePlugin({ settings }: AstroPluginJSXOptions): Plug }, }, }); - return transformJSX({ - code: jsxCode, - id, - renderer: astroJSXRenderer, - mode: viteConfig.mode, - ssr: Boolean(opts?.ssr), - root: settings.config.root, - }); + 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 index b7ae1f2c44ed..3b774a0a238d 100644 --- a/packages/astro/src/vite-plugin-mdx/tag.ts +++ b/packages/astro/src/vite-plugin-mdx/tag.ts @@ -1,5 +1,8 @@ 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. @@ -9,115 +12,100 @@ import * as t from '@babel/types'; * 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. */ -export default async function tagExportsWithRenderer({ - rendererName, -}: { - rendererName: string; - root: URL; -}): Promise { - return { - 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), - ]) - ) - ); - } - } - }, +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') + ) + ); }, - 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), - ]) + // 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), + ]) + ) ); - 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); - } - }); - } + } + }, + }, + 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 new file mode 100644 index 000000000000..07eb87d0465e --- /dev/null +++ b/packages/astro/src/vite-plugin-mdx/transform-jsx.ts @@ -0,0 +1,69 @@ +import babel from '@babel/core'; +import type { TransformResult } from 'rollup'; +import type { JSXTransformConfig } from '../@types/astro.js'; +import { jsxTransformOptions } from '../jsx/transform-options.js'; +import type { PluginMetadata } from '../vite-plugin-astro/types.js'; +import { tagExportsPlugin } from './tag.js'; + +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/units/render/jsx.test.js b/packages/astro/test/units/render/jsx.test.js index 0f91ccfc9712..3e1b01b23e8e 100644 --- a/packages/astro/test/units/render/jsx.test.js +++ b/packages/astro/test/units/render/jsx.test.js @@ -15,6 +15,7 @@ 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;