Skip to content
Open
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
1 change: 1 addition & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Overview of features supported by each editor client.
|---------|:-------:|:--------:|:------:|:---:|
| Success Emoji | ✅ | ✅ | ✅ | ✅ |
| Error Emoji | ✅ | ✅ | ✅ | ✅ |
| Skipped Emoji (`"use no memo"`) | ✅ | ✅ | ✅ | ✅ |
| Babel Plugin Path | ✅ | ✅ | ✅ | ✅ |
| Excluded Directories | ✅ | ✅ | ❌ | ❌ |
| Supported Extensions | ✅ | ✅ | ❌ | ❌ |
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
## Features

- Visual emoji markers next to React components (customizable)
- Respects the `"use no memo"` / `"use no forget"` opt-out directive — opted-out functions are reported as skipped (⏭️) rather than failed
- Hover tooltips with optimization details and error messages
- Preview compiled output to see what the React Compiler generates
- Generate reports for a full-project compilation snapshot
Expand Down
23 changes: 22 additions & 1 deletion packages/cli/src/formatText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ function parseFailure(filePath: string, log: LogEntry): ParsedFailure {
log.detail?.options?.details?.at(0)?.loc?.start?.line ??
log.detail?.options?.loc?.start?.line ??
log.detail?.loc?.start?.line ??
log.loc?.start?.line ??
log.fnLoc?.start?.line ??
undefined;
const reason = log?.detail?.options?.reason || "Unknown reason";
const reason = log?.detail?.options?.reason || log?.reason || "Unknown reason";
return { filePath, fnName: log.fnName, reason, line };
}

Expand All @@ -29,6 +31,7 @@ export function formatText(report: ReactCompilerReport): string {
lines.push(`Files with results: ${totals.filesWithResults}`);
lines.push(`Compiled (success): ${totals.successCount}`);
lines.push(`Failed: ${totals.failedCount}`);
lines.push(`Skipped: ${totals.skippedCount}`);

if (report.errors.length > 0) {
lines.push("");
Expand Down Expand Up @@ -57,6 +60,24 @@ export function formatText(report: ReactCompilerReport): string {
}
}

const skips: ParsedFailure[] = [];
for (const file of report.files) {
for (const log of file.skipped ?? []) {
skips.push(parseFailure(file.path, log));
}
}

if (skips.length > 0) {
lines.push("");
lines.push("Skipped components:");
lines.push("----------------------------------------");
for (const s of skips) {
const loc = s.line ? `:${s.line}` : "";
const name = s.fnName ?? "(anonymous)";
lines.push(` ${s.filePath}${loc} - ${name}: ${s.reason}`);
}
}

lines.push("");
return lines.join("\n");
}
2 changes: 1 addition & 1 deletion packages/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function formatOutput(report: ReactCompilerReport, format: string): string {
const tree = buildReportTree(report);
return getReportHtml({
data: tree,
emojis: { success: "\u2705", error: "\u274C" },
emojis: { success: "\u2705", error: "\u274C", skipped: "\u23ED\uFE0F" },
theme: "auto",
headExtra: `<style>${STANDALONE_CSS}</style>`,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ class GenerateReportAction : AnAction() {
"respectGitignore" to settings.respectGitignore,
"emojis" to mapOf(
"success" to settings.successEmoji,
"error" to settings.errorEmoji
"error" to settings.errorEmoji,
"skipped" to settings.skippedEmoji
)
)

Expand Down Expand Up @@ -136,6 +137,7 @@ class GenerateReportAction : AnAction() {
val listHoverBg = shiftColor(bg, shift)
val success = if (isDark) Color(0x73, 0xC9, 0x91) else Color(0x2E, 0x7D, 0x32)
val failed = if (isDark) Color(0xF4, 0x87, 0x71) else Color(0xC6, 0x28, 0x28)
val skipped = if (isDark) Color(0xB0, 0xB7, 0xC2) else Color(0x5F, 0x66, 0x73)
val fontFamily = UIManager.getFont("Label.font")?.family ?: "sans-serif"
val fontSize = UIManager.getFont("Label.font")?.size ?: 13
val editorFont = UIManager.getFont("EditorPane.font")?.family ?: "monospace"
Expand All @@ -157,6 +159,7 @@ class GenerateReportAction : AnAction() {
--rcm-list-hover-bg: ${colorToCssAlpha(listHoverBg, 0.5)};
--rcm-success: ${colorToCss(success)};
--rcm-failed: ${colorToCss(failed)};
--rcm-skipped: ${colorToCss(skipped)};
--rcm-font-family: '${fontFamily}', sans-serif;
--rcm-font-size: ${fontSize}px;
--rcm-editor-font-family: '${editorFont}', monospace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class ReactCompilerMarkerConfigurable(private val project: Project) : Configurab
private var enabledCheckbox: JBCheckBox? = null
private var successEmojiField: JBTextField? = null
private var errorEmojiField: JBTextField? = null
private var skippedEmojiField: JBTextField? = null
private var babelPluginPathField: JBTextField? = null
private var excludedDirectoriesField: JBTextField? = null
private var supportedExtensionsField: JBTextField? = null
Expand All @@ -26,6 +27,7 @@ class ReactCompilerMarkerConfigurable(private val project: Project) : Configurab
enabledCheckbox = JBCheckBox("Enable React Compiler Marker")
successEmojiField = JBTextField()
errorEmojiField = JBTextField()
skippedEmojiField = JBTextField()
babelPluginPathField = JBTextField()
excludedDirectoriesField = JBTextField()
supportedExtensionsField = JBTextField()
Expand All @@ -36,6 +38,7 @@ class ReactCompilerMarkerConfigurable(private val project: Project) : Configurab
.addSeparator()
.addLabeledComponent(JBLabel("Success emoji:"), successEmojiField!!, 1, false)
.addLabeledComponent(JBLabel("Error emoji:"), errorEmojiField!!, 1, false)
.addLabeledComponent(JBLabel("Skipped emoji:"), skippedEmojiField!!, 1, false)
.addSeparator()
.addLabeledComponent(JBLabel("Babel plugin path:"), babelPluginPathField!!, 1, false)
.addSeparator()
Expand All @@ -52,6 +55,7 @@ class ReactCompilerMarkerConfigurable(private val project: Project) : Configurab
return enabledCheckbox?.isSelected != settings.isEnabled ||
successEmojiField?.text != settings.successEmoji ||
errorEmojiField?.text != settings.errorEmoji ||
skippedEmojiField?.text != settings.skippedEmoji ||
babelPluginPathField?.text != settings.babelPluginPath ||
excludedDirectoriesField?.text != settings.excludedDirectories ||
supportedExtensionsField?.text != settings.supportedExtensions ||
Expand All @@ -63,6 +67,7 @@ class ReactCompilerMarkerConfigurable(private val project: Project) : Configurab
settings.isEnabled = enabledCheckbox?.isSelected ?: true
settings.successEmoji = successEmojiField?.text ?: "\u2728"
settings.errorEmoji = errorEmojiField?.text ?: "\uD83D\uDEAB"
settings.skippedEmoji = skippedEmojiField?.text ?: "\u23ED\uFE0F"
settings.babelPluginPath = babelPluginPathField?.text ?: "node_modules/babel-plugin-react-compiler"
settings.excludedDirectories = excludedDirectoriesField?.text ?: ""
settings.supportedExtensions = supportedExtensionsField?.text ?: ""
Expand All @@ -78,6 +83,7 @@ class ReactCompilerMarkerConfigurable(private val project: Project) : Configurab
enabledCheckbox?.isSelected = settings.isEnabled
successEmojiField?.text = settings.successEmoji
errorEmojiField?.text = settings.errorEmoji
skippedEmojiField?.text = settings.skippedEmoji
babelPluginPathField?.text = settings.babelPluginPath
excludedDirectoriesField?.text = settings.excludedDirectories
supportedExtensionsField?.text = settings.supportedExtensions
Expand All @@ -88,6 +94,7 @@ class ReactCompilerMarkerConfigurable(private val project: Project) : Configurab
enabledCheckbox = null
successEmojiField = null
errorEmojiField = null
skippedEmojiField = null
babelPluginPathField = null
excludedDirectoriesField = null
supportedExtensionsField = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ class ReactCompilerMarkerSettings : PersistentStateComponent<ReactCompilerMarker

class State {
var isEnabled: Boolean = true
var successEmoji: String = "\u2728" // ✨
var errorEmoji: String = "\uD83D\uDEAB" // 🚫
var successEmoji: String = "✨" // ✨
var errorEmoji: String = "🚫" // 🚫
var skippedEmoji: String = "⏭️" // ⏭️
var babelPluginPath: String = "node_modules/babel-plugin-react-compiler"
var excludedDirectories: String = "node_modules, .git, dist, build, out, coverage, .next, .turbo"
var supportedExtensions: String = ".js, .jsx, .ts, .tsx, .mjs, .cjs"
Expand Down Expand Up @@ -47,6 +48,12 @@ class ReactCompilerMarkerSettings : PersistentStateComponent<ReactCompilerMarker
myState.errorEmoji = value
}

var skippedEmoji: String
get() = myState.skippedEmoji
set(value) {
myState.skippedEmoji = value
}

var babelPluginPath: String
get() = myState.babelPluginPath
set(value) {
Expand Down Expand Up @@ -80,6 +87,7 @@ class ReactCompilerMarkerSettings : PersistentStateComponent<ReactCompilerMarker
fun toMap(): Map<String, Any?> = mapOf(
"successEmoji" to successEmoji,
"errorEmoji" to errorEmoji,
"skippedEmoji" to skippedEmoji,
"babelPluginPath" to babelPluginPath,
"excludedDirectories" to excludedDirectoriesList,
"supportedExtensions" to supportedExtensionsList,
Expand Down
3 changes: 3 additions & 0 deletions packages/nvim-client/lua/react-compiler-marker/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ M.defaults = {
success = "✨",
-- Marker for components that failed to optimize
error = "🚫",
-- Marker for components that opted out via "use no memo"
skipped = "⏭️",
},

-- Path to babel-plugin-react-compiler (relative to workspace root)
Expand Down Expand Up @@ -122,6 +124,7 @@ function M.get_server_settings()
reactCompilerMarker = {
successEmoji = M.config.emojis.success,
errorEmoji = M.config.emojis.error,
skippedEmoji = M.config.emojis.skipped,
babelPluginPath = M.config.babel_plugin_path,
},
}
Expand Down
16 changes: 14 additions & 2 deletions packages/server/src/checkReactCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export type LoggerEvent = {
kind?: string;
fnLoc: EventLocation;
fnName?: string;
reason?: string;
loc?: EventLocation;
detail?: Details & {
options: Details;
};
Expand All @@ -52,6 +54,7 @@ export function clearPluginCache(): void {
interface CompilationResult {
successfulCompilations: Array<LoggerEvent>;
failedCompilations: Array<LoggerEvent>;
skippedCompilations: Array<LoggerEvent>;
}

const compilationCache = new LRUCache<CompilationResult>(100);
Expand Down Expand Up @@ -79,6 +82,7 @@ function runBabelPluginReactCompiler(
) {
const successfulCompilations: Array<LoggerEvent> = [];
const failedCompilations: Array<LoggerEvent> = [];
const skippedCompilations: Array<LoggerEvent> = [];

const logger = {
logEvent(filename: string | null, rawEvent: LoggerEvent) {
Expand All @@ -93,6 +97,9 @@ function runBabelPluginReactCompiler(
case "PipelineError":
failedCompilations.push(event);
return;
case "CompileSkip":
skippedCompilations.push(event);
return;
}
},
};
Expand Down Expand Up @@ -126,6 +133,7 @@ function runBabelPluginReactCompiler(
return {
successfulCompilations,
failedCompilations,
skippedCompilations,
};
}

Expand Down Expand Up @@ -180,7 +188,7 @@ export function checkReactCompiler(
const BabelPluginReactCompiler = importBabelPluginReactCompiler(workspaceFolder, babelPluginPath);

if (!BabelPluginReactCompiler) {
return { successfulCompilations: [], failedCompilations: [] };
return { successfulCompilations: [], failedCompilations: [], skippedCompilations: [] };
}

try {
Expand All @@ -198,7 +206,11 @@ export function checkReactCompiler(
return result;
} catch (error: any) {
throttledError(`Failed to compile the file. Please check the file content. ${error?.message}`);
const emptyResult: CompilationResult = { successfulCompilations: [], failedCompilations: [] };
const emptyResult: CompilationResult = {
successfulCompilations: [],
failedCompilations: [],
skippedCompilations: [],
};
compilationCache.set(sourceCode, filename, emptyResult);
return emptyResult;
}
Expand Down
24 changes: 24 additions & 0 deletions packages/server/src/inlayHints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ export function generateInlayHints(
document: TextDocument,
successfulCompilations: LoggerEvent[],
failedCompilations: LoggerEvent[],
skippedCompilations: LoggerEvent[],
successEmoji: string | null,
errorEmoji: string | null,
skippedEmoji: string | null,
documentUri: string,
tooltipFormat: TooltipFormat = "markdown",
clientName?: string
Expand Down Expand Up @@ -119,6 +121,28 @@ export function generateInlayHints(
}
}

// Generate hints for skipped compilations (opt-out via "use no memo")
if (skippedEmoji) {
for (const log of skippedCompilations) {
const positionInfo = getInlayHintPosition(document, log);
if (!positionInfo) {
continue;
}

const f = fmt[tooltipFormat];
const tooltipValue = `${skippedEmoji} ${f.bold(positionInfo.functionName)} has been skipped by React Compiler due to a \`"use no memo"\` directive.`;

const hint: InlayHint = {
position: positionInfo.position,
label: `${skippedEmoji} `,
kind: InlayHintKind.Type,
tooltip: { kind: "markdown", value: tooltipValue },
};

hints.push(hint);
}
}

// Generate hint for failed compilations (one hint with all errors)
if (errorEmoji && failedCompilations.length > 0) {
const f = fmt[tooltipFormat];
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/parseLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export function parseLog(log: LoggerEvent): ParsedLog {
log.detail?.options?.details?.at(0)?.loc?.[property]?.[field] ??
log.detail?.options?.loc?.[property]?.[field] ??
log.detail?.loc?.[property]?.[field] ??
// CompileSkip events expose the directive location at the top level
log.loc?.[property]?.[field] ??
log.fnLoc?.[property]?.[field] ??
defaultValue
);
};
Expand All @@ -30,7 +33,7 @@ export function parseLog(log: LoggerEvent): ParsedLog {
const startChar = getLocValue("start", "column", 0);
const endChar = getLocValue("end", "column", 0);

const reason = log?.detail?.options?.reason || "Unknown reason";
const reason = log?.detail?.options?.reason || log.reason || "Unknown reason";
const description = log?.detail?.options?.description || "";

return {
Expand Down
9 changes: 8 additions & 1 deletion packages/server/src/report/buildTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function buildReportTree(report: ReactCompilerReport): ReportTreeData {
children: [],
successCount: 0,
failedCount: 0,
skippedCount: 0,
};

for (const file of report.files) {
Expand All @@ -34,14 +35,15 @@ export function buildReportTree(report: ReactCompilerReport): ReportTreeData {
children: isFile ? undefined : [],
successCount: 0,
failedCount: 0,
skippedCount: 0,
};
current.children.push(child);
}

if (isFile) {
const toEntry = (
event: (typeof file.success)[number],
kind: "success" | "failure"
kind: NormalizedEntry["kind"]
): NormalizedEntry => {
const parsed = parseLog(event);
return {
Expand All @@ -53,13 +55,16 @@ export function buildReportTree(report: ReactCompilerReport): ReportTreeData {
column: parsed.startChar,
};
};
const skipped = file.skipped ?? [];
const entries: NormalizedEntry[] = [
...file.success.map((event) => toEntry(event, "success")),
...file.failed.map((event) => toEntry(event, "failure")),
...skipped.map((event) => toEntry(event, "skip")),
];
child.entries = entries;
child.successCount = entries.filter((e) => e.kind === "success").length;
child.failedCount = entries.filter((e) => e.kind === "failure").length;
child.skippedCount = entries.filter((e) => e.kind === "skip").length;
}

current = child;
Expand All @@ -85,11 +90,13 @@ function aggregateCounts(node: TreeNode): void {

node.successCount = 0;
node.failedCount = 0;
node.skippedCount = 0;

for (const child of node.children) {
aggregateCounts(child);
node.successCount += child.successCount;
node.failedCount += child.failedCount;
node.skippedCount += child.skippedCount;
}
}

Expand Down
Loading
Loading