Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize JSX import source detection #5498

Merged
merged 2 commits into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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