JSX refactor (#7924)

* JSX refactor

* Get preact/compat test to pass

* Use include config

* Remove old astro flavored markdown test

* Move babel dep to preact

* Remove errant debugger

* Update lockfile

* Update the multi-framework example

* Update e2e tests

* Fix nested-in-vue tests

* Add back in astro check

* Update packages/astro/src/core/create-vite.ts

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* Update packages/astro/src/core/create-vite.ts

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* Update packages/integrations/solid/src/index.ts

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* Update packages/integrations/solid/src/index.ts

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* Update .changeset/perfect-horses-tell.md

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* Move the comment about the include config

* Remove redundant alias config

* Use react's own preamble code

* Use the base for the preamble

* Remove solid redundancy

* Update .changeset/perfect-horses-tell.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update based on review comments

* Oops

---------

Co-authored-by: Fred K. Schott <fkschott@gmail.com>
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Matthew Phillips 2023-08-11 10:05:02 -04:00 committed by GitHub
parent 2ee418e06a
commit 519a1c4e84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
135 changed files with 857 additions and 802 deletions

View file

@ -0,0 +1,7 @@
---
'@astrojs/solid-js': major
---
New `include` and `exclude` config options
The Solid integration now has new `include` and `exclude` config options. Use these if you want to use Solid alongside another JSX framework; include specifies files to be compiled for Solid and `exclude` does the opposite.

View file

@ -0,0 +1,7 @@
---
'@astrojs/preact': major
---
New `include` and `exclude` config options
The Preact integration now has new `include` and `exclude` config options. Use these if you want to use Preact alongside another JSX framework; include specifies files to be compiled for Preact and `exclude` does the opposite.

View file

@ -0,0 +1,27 @@
---
'astro': major
---
Astro's JSX handling has been refactored with better support for each framework.
Previously, Astro automatically scanned your components to determine which framework-specific transformations should be used. In practice, supporting advanced features like Fast Refresh with this approach proved difficult.
Now, Astro determines which framework to use with `include` and `exclude` config options where you can specify files and folders on a per-framework basis. When using multiple JSX frameworks in the same project, users should manually control which files belong to each framework using the `include` and `exclude` options.
```js
export default defineConfig({
// The `include` config is only needed in projects that use multiple JSX frameworks;
// if only using one no extra config is needed.
integrations: [
preact({
include: ['**/preact/*']
}),
react({
include: ['**/react/*']
}),
solid({
include: ['**/solid/*'],
}),
]
});
```

View file

@ -0,0 +1,9 @@
---
'@astrojs/react': major
---
Support for React Refresh
The React integration now fully supports React Refresh and is backed by `@vitejs/plugin-react`.
Also included in this change are new `include` and `exclude` config options. Use these if you want to use React alongside another JSX framework; include specifies files to be compiled for React and `exclude` does the opposite.

View file

@ -8,5 +8,11 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
integrations: [
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
react({ include: ['**/react/*'] }),
svelte(),
vue(),
],
});

View file

@ -4,12 +4,12 @@ import '../styles/global.css';
// Component Imports
// For JSX components, all the common ways of exporting (under a namespace, specific export, default export etc) are supported!
import * as react from '../components/ReactCounter';
import { PreactCounter } from '../components/PreactCounter';
import SolidCounter from '../components/SolidCounter';
import * as react from '../components/react/ReactCounter';
import { PreactCounter } from '../components/preact/PreactCounter';
import SolidCounter from '../components/solid/SolidCounter';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
import VueCounter from '../components/vue/VueCounter.vue';
import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/

View file

@ -75,7 +75,7 @@ test.describe('Error display', () => {
const fileExists = astro.pathExists(absoluteFileUrl);
expect(fileExists).toBeTruthy();
expect(fileLocation).toMatch(/^components\/PreactRuntimeError.jsx/);
expect(fileLocation).toMatch(/^preact\/PreactRuntimeError.jsx/);
});
test('shows correct line when a style preprocess has an error', async ({ page, astro }) => {
@ -105,7 +105,7 @@ test.describe('Error display', () => {
// Wait for page reload
page.waitForNavigation(),
// Edit the component file
astro.editFile('./src/components/SvelteSyntaxError.svelte', () => `<h1>No mismatch</h1>`),
astro.editFile('./src/components/svelte/SvelteSyntaxError.svelte', () => `<h1>No mismatch</h1>`),
]);
expect(await page.locator('vite-error-overlay').count()).toEqual(0);

View file

@ -8,5 +8,11 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
integrations: [
react({ include: ['**/react/*'] }),
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
svelte(),
vue(),
],
});

View file

@ -1,9 +1,9 @@
---
import * as react from '../components/ReactCounter.jsx';
import { PreactCounter } from '../components/PreactCounter.tsx';
import SolidCounter from '../components/SolidCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
import * as react from '../components/react/ReactCounter.jsx';
import { PreactCounter } from '../components/preact/PreactCounter.jsx';
import SolidCounter from '../components/solid/SolidCounter.jsx';
import VueCounter from '../components/vue/VueCounter.vue';
import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/

View file

@ -7,5 +7,11 @@ import vue from '@astrojs/vue';
// https://astro.build/config
export default defineConfig({
integrations: [react(), preact(), solid(), svelte(), vue()],
});
integrations: [
react({ include: ['**/react/*'] }),
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
svelte(),
vue(),
],
});

View file

@ -1,7 +1,7 @@
---
import SvelteDirectiveError from '../components/SvelteDirectiveError.svelte';
import SvelteDirectiveError from '../components/svelte/SvelteDirectiveError.svelte';
---
<div>
<SvelteDirectiveError client:media />
</div>
</div>

View file

@ -1,7 +1,7 @@
---
import SvelteDirectiveError from '../components/SvelteDirectiveError.svelte';
import SvelteDirectiveError from '../components/svelte/SvelteDirectiveError.svelte';
---
<div>
<SvelteDirectiveError client:loadm />
</div>
</div>

View file

@ -1,5 +1,5 @@
---
import PreactRuntimeError from '../components/PreactRuntimeError.jsx';
import PreactRuntimeError from '../components/preact/PreactRuntimeError.jsx';
---
<div>

View file

@ -1,5 +1,5 @@
---
import PreactSyntaxError from '../components/PreactSyntaxError.jsx';
import PreactSyntaxError from '../components/preact/PreactSyntaxError.jsx';
---
<div>

View file

@ -1,5 +1,5 @@
---
import ReactRuntimeError from '../components/ReactRuntimeError.jsx';
import ReactRuntimeError from '../components/react/ReactRuntimeError.jsx';
---
<div>

View file

@ -1,5 +1,5 @@
---
import ReactSyntaxError from '../components/ReactSyntaxError.jsx';
import ReactSyntaxError from '../components/react/ReactSyntaxError.jsx';
---
<div>

View file

@ -1,5 +1,5 @@
---
import SolidRuntimeError from '../components/SolidRuntimeError.jsx';
import SolidRuntimeError from '../components/solid/SolidRuntimeError.jsx';
---
<div>

View file

@ -1,5 +1,5 @@
---
import SolidSyntaxError from '../components/SolidSyntaxError.jsx';
import SolidSyntaxError from '../components/solid/SolidSyntaxError.jsx';
---
<div>

View file

@ -1,5 +1,5 @@
---
import SvelteRuntimeError from '../components/SvelteRuntimeError.svelte';
import SvelteRuntimeError from '../components/svelte/SvelteRuntimeError.svelte';
---
<div>

View file

@ -1,5 +1,5 @@
---
import SvelteSyntaxError from '../components/SvelteSyntaxError.svelte';
import SvelteSyntaxError from '../components/svelte/SvelteSyntaxError.svelte';
---
<div>

View file

@ -1,5 +1,5 @@
---
import VueRuntimeError from '../components/VueRuntimeError.vue';
import VueRuntimeError from '../components/vue/VueRuntimeError.vue';
---
<div>

View file

@ -1,5 +1,5 @@
---
import VueSyntaxError from '../components/VueSyntaxError.vue';
import VueSyntaxError from '../components/vue/VueSyntaxError.vue';
---
<div>

View file

@ -8,5 +8,11 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
integrations: [
react({ include: ['**/react/*'] }),
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
svelte(),
vue(),
],
});

View file

@ -3,11 +3,11 @@
import '../styles/global.css';
// Component Imports
import { A, B as Renamed } from '../components';
import * as react from '../components/ReactCounter.jsx';
import { PreactCounter } from '../components/PreactCounter.tsx';
import SolidCounter from '../components/SolidCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
import * as react from '../components/react/ReactCounter.jsx';
import { PreactCounter } from '../components/preact/PreactCounter.tsx';
import SolidCounter from '../components/solid/SolidCounter.tsx';
import VueCounter from '../components/vue/VueCounter.vue';
import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/

View file

@ -8,5 +8,11 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
integrations: [
react({ include: ['**/react/*'] }),
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
svelte(),
vue(),
],
});

View file

@ -1,9 +1,9 @@
---
import ReactCounter from '../components/ReactCounter.jsx';
import { PreactCounter } from '../components/PreactCounter.tsx';
import SolidCounter from '../components/SolidCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
import ReactCounter from '../components/react/ReactCounter.jsx';
import { PreactCounter } from '../components/preact/PreactCounter.tsx';
import SolidCounter from '../components/solid/SolidCounter.tsx';
import VueCounter from '../components/vue/VueCounter.vue';
import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/

View file

@ -8,5 +8,11 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
integrations: [
react({ include: ['**/react/*'] }),
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
svelte(),
vue(),
],
});

View file

@ -1,9 +1,9 @@
---
import ReactCounter from '../components/ReactCounter.jsx';
import { PreactCounter } from '../components/PreactCounter.tsx';
import SolidCounter from '../components/SolidCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
import ReactCounter from '../components/react/ReactCounter.jsx';
import { PreactCounter } from '../components/preact/PreactCounter.tsx';
import SolidCounter from '../components/solid/SolidCounter.tsx';
import VueCounter from '../components/vue/VueCounter.vue';
import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/

View file

@ -8,5 +8,11 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
integrations: [
react({ include: ['**/react/*'] }),
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
svelte(),
vue(),
],
});

View file

@ -1,9 +1,9 @@
---
import { Counter as ReactCounter } from '../components/ReactCounter.jsx';
import { PreactCounter } from '../components/PreactCounter.tsx';
import SolidCounter from '../components/SolidCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
import { Counter as ReactCounter } from '../components/react/ReactCounter.jsx';
import { PreactCounter } from '../components/preact/PreactCounter.tsx';
import SolidCounter from '../components/solid/SolidCounter.tsx';
import VueCounter from '../components/vue/VueCounter.vue';
import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/

View file

@ -8,5 +8,11 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
integrations: [
react({ include: ['**/react/*'] }),
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
svelte(),
vue(),
],
});

View file

@ -1,9 +1,9 @@
---
import { Counter as ReactCounter } from '../components/ReactCounter.jsx';
import { PreactCounter } from '../components/PreactCounter.tsx';
import SolidCounter from '../components/SolidCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
import { Counter as ReactCounter } from '../components/react/ReactCounter.jsx';
import { PreactCounter } from '../components/preact/PreactCounter.tsx';
import SolidCounter from '../components/solid/SolidCounter.tsx';
import VueCounter from '../components/vue/VueCounter.vue';
import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/

View file

@ -8,5 +8,11 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
integrations: [
react({ include: ['**/react/*'] }),
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
svelte(),
vue(),
],
});

View file

@ -1,9 +1,9 @@
---
import { Counter as ReactCounter } from '../components/ReactCounter.jsx';
import { PreactCounter } from '../components/PreactCounter.tsx';
import SolidCounter from '../components/SolidCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
import { Counter as ReactCounter } from '../components/react/ReactCounter.jsx';
import { PreactCounter } from '../components/preact/PreactCounter.tsx';
import SolidCounter from '../components/solid/SolidCounter.tsx';
import VueCounter from '../components/vue/VueCounter.vue';
import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/

View file

@ -8,5 +8,11 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
integrations: [
react({ include: ['**/react/*'] }),
preact({ include: ['**/preact/*'] }),
solid({ include: ['**/solid/*'] }),
svelte(),
vue(),
],
});

View file

@ -1,9 +1,9 @@
---
import ReactCounter from '../components/ReactCounter.jsx';
import PreactCounter from '../components/PreactCounter.tsx';
import SolidCounter from '../components/SolidCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
import ReactCounter from '../components/react/ReactCounter.jsx';
import PreactCounter from '../components/preact/PreactCounter.tsx';
import SolidCounter from '../components/solid/SolidCounter.tsx';
import VueCounter from '../components/vue/VueCounter.vue';
import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
---
<html lang="en">

View file

@ -38,6 +38,8 @@ export function vitePluginRenderers(opts: StaticBuildOptions): VitePlugin {
exports.push(`export const renderers = [${rendererItems}];`);
return `${imports.join('\n')}\n${exports.join('\n')}`;
} else {
return `export const renderers = [];`;
}
}
},

View file

@ -3,7 +3,6 @@ import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { AstroConfig, AstroSettings } from '../../@types/astro';
import { getContentPaths } from '../../content/index.js';
import jsxRenderer from '../../jsx/renderer.js';
import { markdownContentEntryType } from '../../vite-plugin-markdown/content-entry-type.js';
import { getDefaultClientDirectives } from '../client-directive/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
@ -96,7 +95,7 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
},
},
],
renderers: [jsxRenderer],
renderers: [],
scripts: [],
clientDirectives: getDefaultClientDirectives(),
watchFiles: [],

View file

@ -19,9 +19,9 @@ import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
import envVitePlugin from '../vite-plugin-env/index.js';
import astroHeadPlugin from '../vite-plugin-head/index.js';
import htmlVitePlugin from '../vite-plugin-html/index.js';
import mdxVitePlugin from '../vite-plugin-mdx/index.js';
import { astroInjectEnvTsPlugin } from '../vite-plugin-inject-env-ts/index.js';
import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js';
import jsxVitePlugin from '../vite-plugin-jsx/index.js';
import astroLoadFallbackPlugin from '../vite-plugin-load-fallback/index.js';
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
import astroScannerPlugin from '../vite-plugin-scanner/index.js';
@ -121,7 +121,7 @@ export async function createVite(
envVitePlugin({ settings }),
markdownVitePlugin({ settings, logging }),
htmlVitePlugin(),
jsxVitePlugin({ settings, logging }),
mdxVitePlugin({ settings, logging }),
astroPostprocessVitePlugin(),
astroIntegrationsContainerPlugin({ settings, logging }),
astroScriptsPageSSRPlugin({ settings }),

View file

@ -1,251 +0,0 @@
import type { TransformResult } from 'rollup';
import {
transformWithEsbuild,
type EsbuildTransformOptions,
type Plugin,
type ResolvedConfig,
} from 'vite';
import type { AstroRenderer, AstroSettings } from '../@types/astro';
import type { LogOptions } from '../core/logger/core.js';
import type { PluginMetadata } from '../vite-plugin-astro/types';
import babel from '@babel/core';
import * as colors from 'kleur/colors';
import path from 'node:path';
import { CONTENT_FLAG, PROPAGATED_ASSET_FLAG } from '../content/index.js';
import { astroEntryPrefix } from '../core/build/plugins/plugin-component-entry.js';
import { error } from '../core/logger/core.js';
import { removeQueryString } from '../core/path.js';
import { detectImportSource } from './import-source.js';
import tagExportsPlugin from './tag.js';
const JSX_EXTENSIONS = new Set(['.jsx', '.tsx', '.mdx']);
const IMPORT_STATEMENTS: Record<string, string> = {
react: "import React from 'react'",
preact: "import { h } from 'preact'",
'solid-js': "import 'solid-js'",
astro: "import 'astro/jsx-runtime'",
};
function getEsbuildLoader(filePath: string): EsbuildTransformOptions['loader'] {
const fileExt = path.extname(filePath);
if (fileExt === '.mdx') return 'jsx';
return fileExt.slice(1) as EsbuildTransformOptions['loader'];
}
function collectJSXRenderers(renderers: AstroRenderer[]): Map<string, AstroRenderer> {
const renderersWithJSXSupport = renderers.filter((r) => r.jsxImportSource);
return new Map(
renderersWithJSXSupport.map((r) => [r.jsxImportSource, r] as [string, AstroRenderer])
);
}
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;
logging: LogOptions;
}
// 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 jsx({ settings, logging }: AstroPluginJSXOptions): Plugin {
let viteConfig: ResolvedConfig;
const jsxRenderers = new Map<string, AstroRenderer>();
const jsxRenderersIntegrationOnly = new Map<string, AstroRenderer>();
// A reference to Astro's internal JSX renderer.
let astroJSXRenderer: AstroRenderer;
// The first JSX renderer provided is considered the default renderer.
// This is a useful reference for when the user only gives a single render.
let defaultJSXRendererEntry: [string, AstroRenderer] | undefined;
return {
name: 'astro:jsx',
enforce: 'pre', // run transforms before other plugins
async configResolved(resolvedConfig) {
viteConfig = resolvedConfig;
const possibleRenderers = collectJSXRenderers(settings.renderers);
for (const [importSource, renderer] of possibleRenderers) {
jsxRenderers.set(importSource, renderer);
if (importSource === 'astro') {
astroJSXRenderer = renderer;
} else {
jsxRenderersIntegrationOnly.set(importSource, renderer);
}
}
defaultJSXRendererEntry = [...jsxRenderersIntegrationOnly.entries()][0];
},
async transform(code, id, opts) {
const ssr = Boolean(opts?.ssr);
// 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);
if (!JSX_EXTENSIONS.has(path.extname(id))) {
return null;
}
const { mode } = viteConfig;
// Shortcut: only use Astro renderer for MD and MDX files
if (id.endsWith('.mdx')) {
const { code: jsxCode } = await transformWithEsbuild(code, id, {
loader: getEsbuildLoader(id),
jsx: 'preserve',
sourcemap: 'inline',
tsconfigRaw: {
compilerOptions: {
// Ensure client:only imports are treeshaken
verbatimModuleSyntax: false,
importsNotUsedAsValues: 'remove',
},
},
});
return transformJSX({
code: jsxCode,
id,
renderer: astroJSXRenderer,
mode,
ssr,
root: settings.config.root,
});
}
if (defaultJSXRendererEntry && jsxRenderersIntegrationOnly.size === 1) {
// downlevel any non-standard syntax, but preserve JSX
const { code: jsxCode } = await transformWithEsbuild(code, id, {
loader: getEsbuildLoader(id),
jsx: 'preserve',
sourcemap: 'inline',
});
return transformJSX({
code: jsxCode,
id,
renderer: defaultJSXRendererEntry[1],
mode,
ssr,
root: settings.config.root,
});
}
const importSource = await detectImportSource(code, jsxRenderers, settings.tsConfig);
// if we still cant tell the import source, now is the time to throw an error.
if (!importSource && defaultJSXRendererEntry) {
const [defaultRendererName] = defaultJSXRendererEntry;
error(
logging,
'renderer',
`${colors.yellow(id)}
Unable to resolve a renderer that handles this file! With more than one renderer enabled, you should include an import or use a pragma comment.
Add ${colors.cyan(
IMPORT_STATEMENTS[defaultRendererName] || `import '${defaultRendererName}';`
)} or ${colors.cyan(`/** @jsxImportSource: ${defaultRendererName} */`)} to this file.
`
);
return null;
} else if (!importSource) {
error(
logging,
'renderer',
`${colors.yellow(id)}
Unable to find a renderer for JSX. Do you have one configured in your Astro config? See this page to learn how:
https://docs.astro.build/en/core-concepts/framework-components/#installing-integrations
`
);
return null;
}
const selectedJsxRenderer = jsxRenderers.get(importSource);
// if the renderer is not installed for this JSX source, throw error
if (!selectedJsxRenderer) {
error(
logging,
'renderer',
`${colors.yellow(
id
)} No renderer installed for ${importSource}. Try adding \`@astrojs/${importSource}\` to your project.`
);
return null;
}
// downlevel any non-standard syntax, but preserve JSX
const { code: jsxCode } = await transformWithEsbuild(code, id, {
loader: getEsbuildLoader(id),
jsx: 'preserve',
sourcemap: 'inline',
});
return await transformJSX({
code: jsxCode,
id,
renderer: selectedJsxRenderer,
mode,
ssr,
root: settings.config.root,
});
},
};
}

View file

@ -49,11 +49,6 @@ function safeMatter(source: string, id: string) {
}
}
// absolute path of "astro/jsx-runtime"
const astroJsxRuntimeModulePath = normalizePath(
fileURLToPath(new URL('../jsx-runtime/index.js', import.meta.url))
);
const astroServerRuntimeModulePath = normalizePath(
fileURLToPath(new URL('../runtime/server/index.js', import.meta.url))
);
@ -115,8 +110,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
}
const code = escapeViteEnvReferences(`
import { Fragment, jsx as h } from ${JSON.stringify(astroJsxRuntimeModulePath)};
import { spreadAttributes } from ${JSON.stringify(astroServerRuntimeModulePath)};
import { unescapeHTML, spreadAttributes, createComponent, render, renderComponent } from ${JSON.stringify(astroServerRuntimeModulePath)};
import { AstroError, AstroErrorData } from ${JSON.stringify(astroErrorModulePath)};
${layout ? `import Layout from ${JSON.stringify(layout)};` : ''}
@ -167,27 +161,29 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
export function getHeadings() {
return ${JSON.stringify(headings)};
}
export async function Content() {
export const Content = createComponent((result, _props, slots) => {
const { layout, ...content } = frontmatter;
content.file = file;
content.url = url;
const contentFragment = h(Fragment, { 'set:html': html });
return ${
layout
? `h(Layout, {
file,
url,
content,
frontmatter: content,
headings: getHeadings(),
rawContent,
compiledContent,
'server:root': true,
children: contentFragment
})`
: `contentFragment`
};
}
? `render\`\${renderComponent(result, 'Layout', Layout, {
file,
url,
content,
frontmatter: content,
headings: getHeadings(),
rawContent,
compiledContent,
'server:root': true,
}, {
'default': () => render\`\${unescapeHTML(html)}\`
})}\`;`
: `render\`\${unescapeHTML(html)}\`;`
}
});
Content[Symbol.for('astro.needsHeadRendering')] = ${layout ? 'false' : 'true'};
export default Content;
`);

View file

@ -0,0 +1,134 @@
import type { TransformResult } from 'rollup';
import {
transformWithEsbuild,
type Plugin,
type ResolvedConfig,
} from 'vite';
import type { AstroRenderer, AstroSettings } from '../@types/astro';
import type { LogOptions } from '../core/logger/core.js';
import type { PluginMetadata } from '../vite-plugin-astro/types';
import babel from '@babel/core';
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<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;
logging: LogOptions;
}
// 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;
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.
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 transformJSX({
code: jsxCode,
id,
renderer: astroJSXRenderer,
mode: viteConfig.mode,
ssr: Boolean(opts?.ssr),
root: settings.config.root,
});
},
};
}

Some files were not shown because too many files have changed in this diff Show more