Skip to content

Commit

Permalink
Optimize JSX import source detection (#5498)
Browse files Browse the repository at this point in the history
* Optimize JSX import source detection

* Skip type import check
  • Loading branch information
bluwy authored Nov 30, 2022
1 parent ca01a71 commit 1a3923d
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 68 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-dolphins-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Optimize JSX import source detection
59 changes: 59 additions & 0 deletions packages/astro/src/vite-plugin-jsx/import-source.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
70 changes: 2 additions & 68 deletions packages/astro/src/vite-plugin-jsx/index.ts
Original file line number Diff line number Diff line change
@@ -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> = {
Expand All @@ -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/;

Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 1a3923d

Please sign in to comment.