Optimize JSX import source detection (#5498)

* Optimize JSX import source detection

* Skip type import check
This commit is contained in:
Bjorn Lu 2022-12-01 00:01:37 +08:00 committed by GitHub
parent ca01a71eb0
commit 1a3923da78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 66 additions and 68 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Optimize JSX import source detection

View 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();
}
}
}

View file

@ -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 cant tell the import source, now is the time to throw an error.
if (!importSource && defaultJSXRendererEntry) {