diff options
author | 2022-12-01 00:01:37 +0800 | |
---|---|---|
committer | 2022-11-30 11:01:37 -0500 | |
commit | 1a3923da780288e6435fa79ee8fb61e420af344c (patch) | |
tree | 54419f4605d8b1ff593405bf79271f58ae52ae49 | |
parent | ca01a71eb0937eae3ddc34d48396df8161e830db (diff) | |
download | astro-1a3923da780288e6435fa79ee8fb61e420af344c.tar.gz astro-1a3923da780288e6435fa79ee8fb61e420af344c.tar.zst astro-1a3923da780288e6435fa79ee8fb61e420af344c.zip |
Optimize JSX import source detection (#5498)
* Optimize JSX import source detection
* Skip type import check
-rw-r--r-- | .changeset/wicked-dolphins-design.md | 5 | ||||
-rw-r--r-- | packages/astro/src/vite-plugin-jsx/import-source.ts | 59 | ||||
-rw-r--r-- | packages/astro/src/vite-plugin-jsx/index.ts | 70 |
3 files changed, 66 insertions, 68 deletions
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<string, AstroRenderer>, + tsConfig?: TsConfigJson +): Promise<string | undefined> { + 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 = + /(?<!\/\/.*)(?<=^|;|\*\/)\s*(?:import(?!\s+type)(?:[\w*{}\n\r\t, ]+from)?\s*("[^"]+"|'[^']+')\s*(?=$|;|\/\/|\/\*)|import\s*\(\s*("[^"]+"|'[^']+')\s*\))/gm; + +/** + * Scan a file's imports to detect which renderer it may need. + * ex: if the file imports "preact", it's safe to assume the + * component should be built as a Preact component. + * If no relevant imports found, return undefined. + */ +async function detectImportSourceFromImports( + code: string, + jsxRenderers: Map<string, AstroRenderer> +): Promise<string | undefined> { + 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<string, string> = { @@ -27,10 +21,6 @@ const IMPORT_STATEMENTS: Record<string, string> = { 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<string, AstroRende ); } -/** - * 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(); - } - } -} - -/** - * Scan a file's imports to detect which renderer it may need. - * ex: if the file imports "preact", it's safe to assume the - * component should be built as a Preact component. - * If no relevant imports found, return undefined. - */ -async function detectImportSourceFromImports( - code: string, - id: string, - jsxRenderers: Map<string, AstroRenderer> -) { - // 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) { |