0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-02-03 22:29:08 -05:00

Refactor MDX transformJSX handling (#10688)

This commit is contained in:
Bjorn Lu 2024-04-08 13:35:41 +08:00 committed by GitHub
parent 4ea042c388
commit 799f6f3f29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 199 additions and 217 deletions

View file

@ -0,0 +1,5 @@
---
"astro": patch
---
Marks renderer `jsxImportSource` and `jsxTransformOptions` options as deprecated as they are no longer used since Astro 3.0

View file

@ -2577,9 +2577,9 @@ export interface AstroRenderer {
clientEntrypoint?: string; clientEntrypoint?: string;
/** Import entrypoint for the server/build/ssr renderer. */ /** Import entrypoint for the server/build/ssr renderer. */
serverEntrypoint: string; serverEntrypoint: string;
/** JSX identifier (e.g. 'react' or 'solid-js') */ /** @deprecated Vite plugins should transform the JSX instead */
jsxImportSource?: string; jsxImportSource?: string;
/** Babel transform options */ /** @deprecated Vite plugins should transform the JSX instead */
jsxTransformOptions?: JSXTransformFn; jsxTransformOptions?: JSXTransformFn;
} }

View file

@ -137,7 +137,7 @@ export async function createVite(
envVitePlugin({ settings }), envVitePlugin({ settings }),
markdownVitePlugin({ settings, logger }), markdownVitePlugin({ settings, logger }),
htmlVitePlugin(), htmlVitePlugin(),
mdxVitePlugin({ settings, logger }), mdxVitePlugin(),
astroPostprocessVitePlugin(), astroPostprocessVitePlugin(),
astroIntegrationsContainerPlugin({ settings, logger }), astroIntegrationsContainerPlugin({ settings, logger }),
astroScriptsPageSSRPlugin({ settings }), astroScriptsPageSSRPlugin({ settings }),

View file

@ -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', name: 'astro:jsx',
serverEntrypoint: 'astro/jsx/server.js', serverEntrypoint: 'astro/jsx/server.js',
jsxImportSource: 'astro', jsxImportSource: 'astro',
jsxTransformOptions: async () => { jsxTransformOptions,
// @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' }),
],
};
},
}; };
export default renderer; export default renderer;

View file

@ -0,0 +1,14 @@
import type { JSXTransformConfig } from '../@types/astro.js';
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

@ -1,3 +1,3 @@
# vite-plugin-jsx # vite-plugin-mdx
Modifies Vites 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.

View file

@ -1,99 +1,19 @@
import type { TransformResult } from 'rollup'; import { type Plugin, transformWithEsbuild } from 'vite';
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 { CONTENT_FLAG, PROPAGATED_ASSET_FLAG } from '../content/index.js'; import { CONTENT_FLAG, PROPAGATED_ASSET_FLAG } from '../content/index.js';
import { astroEntryPrefix } from '../core/build/plugins/plugin-component-entry.js'; import { astroEntryPrefix } from '../core/build/plugins/plugin-component-entry.js';
import { removeQueryString } from '../core/path.js'; import { removeQueryString } from '../core/path.js';
import tagExportsPlugin from './tag.js'; import { transformJSX } from './transform-jsx.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<TransformResult> {
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;
}
// Format inspired by https://github.com/vitejs/vite/blob/main/packages/vite/src/node/constants.ts#L54 // Format inspired by https://github.com/vitejs/vite/blob/main/packages/vite/src/node/constants.ts#L54
const SPECIAL_QUERY_REGEX = new RegExp( const SPECIAL_QUERY_REGEX = new RegExp(
`[?&](?:worker|sharedworker|raw|url|${CONTENT_FLAG}|${PROPAGATED_ASSET_FLAG})\\b` `[?&](?: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) */ // TODO: Move this Vite plugin into `@astrojs/mdx` in Astro 5
export default function mdxVitePlugin({ settings }: AstroPluginJSXOptions): Plugin { export default function mdxVitePlugin(): Plugin {
let viteConfig: ResolvedConfig;
// A reference to Astro's internal JSX renderer.
let astroJSXRenderer: AstroRenderer;
return { return {
name: 'astro:jsx', name: 'astro:jsx',
enforce: 'pre', // run transforms before other plugins 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) { async transform(code, id, opts) {
// Skip special queries and astro entries. We skip astro entries here as we know it doesn't contain // 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. // 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({ return await transformJSX(jsxCode, id, opts?.ssr);
code: jsxCode,
id,
renderer: astroJSXRenderer,
mode: viteConfig.mode,
ssr: Boolean(opts?.ssr),
root: settings.config.root,
});
}, },
}; };
} }

View file

@ -1,5 +1,8 @@
import type { PluginObj } from '@babel/core'; import type { PluginObj } from '@babel/core';
import * as t from '@babel/types'; 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. * 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 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. * This allows us to automatically match a component to a renderer and skip the usual `check()` calls.
*/ */
export default async function tagExportsWithRenderer({ export const tagExportsPlugin: PluginObj = {
rendererName, visitor: {
}: { Program: {
rendererName: string; // Inject `import { __astro_tag_component__ } from 'astro/runtime/server/index.js'`
root: URL; enter(path) {
}): Promise<PluginObj> { path.node.body.splice(
return { 0,
visitor: { 0,
Program: { t.importDeclaration(
// Inject `import { __astro_tag_component__ } from 'astro/runtime/server/index.js'` [
enter(path) { t.importSpecifier(
path.node.body.splice( t.identifier('__astro_tag_component__'),
0, t.identifier('__astro_tag_component__')
0, ),
t.importDeclaration( ],
[ t.stringLiteral('astro/runtime/server/index.js')
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 each export we found, inject `__astro_tag_component__(exportName, rendererName)`
/** exit(path, state) {
* For default anonymous function export, we need to give them a unique name const exportedIds = state.get('astro:tags');
* @param path if (exportedIds) {
* @returns for (const id of exportedIds) {
*/ path.node.body.push(
enter(path) { t.expressionStatement(
const node = path.node; t.callExpression(t.identifier('__astro_tag_component__'), [
if (!t.isExportDefaultDeclaration(node)) return; t.identifier(id),
t.stringLiteral(rendererName),
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);
}
});
}
}
},
}, },
}, },
}; 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

@ -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<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

@ -15,6 +15,7 @@ import { createBasicPipeline } from '../test-utils.js';
const createAstroModule = (AstroComponent) => ({ default: AstroComponent }); const createAstroModule = (AstroComponent) => ({ default: AstroComponent });
const loadJSXRenderer = () => loadRenderer(jsxRenderer, { import: (s) => import(s) }); const loadJSXRenderer = () => loadRenderer(jsxRenderer, { import: (s) => import(s) });
// NOTE: This test may be testing an outdated JSX setup
describe('core/render', () => { describe('core/render', () => {
describe('Astro JSX components', () => { describe('Astro JSX components', () => {
let pipeline; let pipeline;