diff --git a/.changeset/wicked-dolphins-design.md b/.changeset/wicked-dolphins-design.md new file mode 100644 index 000000000..96236ebb4 --- /dev/null +++ b/.changeset/wicked-dolphins-design.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Optimize JSX import source detection diff --git a/packages/astro/src/vite-plugin-jsx/import-source.ts b/packages/astro/src/vite-plugin-jsx/import-source.ts new file mode 100644 index 000000000..f21d2ba02 --- /dev/null +++ b/packages/astro/src/vite-plugin-jsx/import-source.ts @@ -0,0 +1,59 @@ +import { TsConfigJson } from 'tsconfig-resolver'; +import { AstroRenderer } from '../@types/astro'; +import { parseNpmName } from '../core/util.js'; + +export async function detectImportSource( + code: string, + jsxRenderers: Map, + tsConfig?: TsConfigJson +): Promise { + let importSource = detectImportSourceFromComments(code); + if (!importSource && /import/.test(code)) { + importSource = await detectImportSourceFromImports(code, jsxRenderers); + } + if (!importSource && tsConfig) { + importSource = tsConfig.compilerOptions?.jsxImportSource; + } + return importSource; +} + +// Matches import statements and dynamic imports. Captures import specifiers only. +// Adapted from: https://github.com/vitejs/vite/blob/97f8b4df3c9eb817ab2669e5c10b700802eec900/packages/vite/src/node/optimizer/scan.ts#L47-L48 +const importsRE = + /(? +): Promise { + let m; + importsRE.lastIndex = 0; + while ((m = importsRE.exec(code)) != null) { + const spec = (m[1] || m[2]).slice(1, -1); + const pkg = parseNpmName(spec); + if (pkg && jsxRenderers.has(pkg.name)) { + return pkg.name; + } + } +} + +/** + * Scan a file for an explicit @jsxImportSource comment. + * If one is found, return it's value. Otherwise, return undefined. + */ +function detectImportSourceFromComments(code: string): string | undefined { + // if no imports were found, look for @jsxImportSource comment + const multiline = code.match(/\/\*\*?[\S\s]*\*\//gm) || []; + for (const comment of multiline) { + const [_, lib] = comment.slice(0, -2).match(/@jsxImportSource\s*(\S+)/) || []; + if (lib) { + return lib.trim(); + } + } +} diff --git a/packages/astro/src/vite-plugin-jsx/index.ts b/packages/astro/src/vite-plugin-jsx/index.ts index 0a3e64344..39be6db6e 100644 --- a/packages/astro/src/vite-plugin-jsx/index.ts +++ b/packages/astro/src/vite-plugin-jsx/index.ts @@ -1,23 +1,17 @@ import type { TransformResult } from 'rollup'; -import type { TsConfigJson } from 'tsconfig-resolver'; import type { Plugin, 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 eslexer from 'es-module-lexer'; import esbuild from 'esbuild'; import * as colors from 'kleur/colors'; import path from 'path'; import { error } from '../core/logger/core.js'; import { removeQueryString } from '../core/path.js'; -import { parseNpmName } from '../core/util.js'; import tagExportsPlugin from './tag.js'; - -type FixedCompilerOptions = TsConfigJson.CompilerOptions & { - jsxImportSource?: string; -}; +import { detectImportSource } from './import-source.js'; const JSX_EXTENSIONS = new Set(['.jsx', '.tsx', '.mdx']); const IMPORT_STATEMENTS: Record = { @@ -27,10 +21,6 @@ const IMPORT_STATEMENTS: Record = { astro: "import 'astro/jsx-runtime'", }; -// A code snippet to inject into JS files to prevent esbuild reference bugs. -// The `tsx` loader in esbuild will remove unused imports, so we need to -// be careful about esbuild not treating h, React, Fragment, etc. as unused. -const PREVENT_UNUSED_IMPORTS = ';;(React,Fragment,h);'; // A fast check regex for the import keyword. False positives are okay. const IMPORT_KEYWORD_REGEX = /import/; @@ -46,53 +36,6 @@ function collectJSXRenderers(renderers: AstroRenderer[]): Map -) { - // We need valid JS to scan for imports. - // NOTE: Because we only need imports, it is okay to use `h` and `Fragment` as placeholders. - const { code: jsCode } = await esbuild.transform(code + PREVENT_UNUSED_IMPORTS, { - loader: getEsbuildLoader(path.extname(id)) as esbuild.Loader, - jsx: 'transform', - jsxFactory: 'h', - jsxFragment: 'Fragment', - sourcefile: id, - sourcemap: 'inline', - }); - const [imports] = eslexer.parse(jsCode); - if (imports.length > 0) { - for (let { n: spec } of imports) { - const pkg = spec && parseNpmName(spec); - if (!pkg) continue; - if (jsxRenderers.has(pkg.name)) { - return pkg.name; - } - } - } -} interface TransformJSXOptions { code: string; id: string; @@ -229,16 +172,7 @@ export default function jsx({ settings, logging }: AstroPluginJSXOptions): Plugi }); } - let importSource = detectImportSourceFromComments(code); - if (!importSource && IMPORT_KEYWORD_REGEX.test(code)) { - importSource = await detectImportSourceFromImports(code, id, jsxRenderers); - } - - // Check the tsconfig - if (!importSource) { - const compilerOptions = settings.tsConfig?.compilerOptions; - importSource = (compilerOptions as FixedCompilerOptions | undefined)?.jsxImportSource; - } + const importSource = await detectImportSource(code, jsxRenderers, settings.tsConfig); // if we still can’t tell the import source, now is the time to throw an error. if (!importSource && defaultJSXRendererEntry) {