mirror of
https://github.com/withastro/astro.git
synced 2025-03-17 23:11:29 -05:00
Cleanup unused JSX code (#11741)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
f239242d90
commit
6617491c3b
21 changed files with 42 additions and 772 deletions
14
.changeset/many-garlics-lick.md
Normal file
14
.changeset/many-garlics-lick.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
'astro': major
|
||||
---
|
||||
|
||||
Removes internal JSX handling and moves the responsibility to the `@astrojs/mdx` package directly. The following exports are also now removed:
|
||||
|
||||
- `astro/jsx/babel.js`
|
||||
- `astro/jsx/component.js`
|
||||
- `astro/jsx/index.js`
|
||||
- `astro/jsx/renderer.js`
|
||||
- `astro/jsx/server.js`
|
||||
- `astro/jsx/transform-options.js`
|
||||
|
||||
If your project includes `.mdx` files, you must upgrade `@astrojs/mdx` to the latest version so that it doesn't rely on these entrypoints to handle your JSX.
|
7
.changeset/perfect-fans-fly.md
Normal file
7
.changeset/perfect-fans-fly.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
'@astrojs/mdx': minor
|
||||
---
|
||||
|
||||
Updates adapter server entrypoint to use `@astrojs/mdx/server.js`
|
||||
|
||||
This is an internal change. Handling JSX in your `.mdx` files has been moved from Astro internals and is now the responsibility of this integration. You should not notice a change in your project, and no update to your code is required.
|
|
@ -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"
|
||||
|
|
|
@ -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 },
|
||||
),
|
||||
];
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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)),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { createAstroJSXComponent } from './component.js';
|
||||
export { default as renderer } from './renderer.js';
|
|
@ -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;
|
|
@ -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' }),
|
||||
],
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# vite-plugin-mdx
|
||||
|
||||
Handles transforming MDX via the `astro:jsx` renderer.
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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": [
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue