Optimize JSX import source detection (#5498)
* Optimize JSX import source detection * Skip type import check
This commit is contained in:
parent
ca01a71eb0
commit
1a3923da78
3 changed files with 66 additions and 68 deletions
5
.changeset/wicked-dolphins-design.md
Normal file
5
.changeset/wicked-dolphins-design.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Optimize JSX import source detection
|
59
packages/astro/src/vite-plugin-jsx/import-source.ts
Normal file
59
packages/astro/src/vite-plugin-jsx/import-source.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue