diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index e0c670c5641cd..0adbf3077c285 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -182,7 +182,9 @@ export type LoggerEvent = | CompileDiagnosticEvent | CompileSkipEvent | PipelineErrorEvent - | TimingEvent; + | TimingEvent + | AutoDepsDecorationsEvent + | AutoDepsEligibleEvent; export type CompileErrorEvent = { kind: 'CompileError'; @@ -219,6 +221,16 @@ export type TimingEvent = { kind: 'Timing'; measurement: PerformanceMeasure; }; +export type AutoDepsDecorationsEvent = { + kind: 'AutoDepsDecorations'; + fnLoc: t.SourceLocation; + decorations: Array; +}; +export type AutoDepsEligibleEvent = { + kind: 'AutoDepsEligible'; + fnLoc: t.SourceLocation; + depArrayLoc: t.SourceLocation; +}; export type Logger = { logEvent: (filename: string | null, event: LoggerEvent) => void; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index 61f57f68cfeb9..8dfdc76978cc2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -392,6 +392,11 @@ function runWithEnvironment( if (env.config.inferEffectDependencies) { inferEffectDependencies(hir); + log({ + kind: 'hir', + name: 'InferEffectDependencies', + value: hir, + }); } if (env.config.inlineJsxTransform) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Reanimated.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Reanimated.ts index e946d7aab88c0..4f8a3e709d1c1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Reanimated.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Reanimated.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import type * as BabelCore from '@babel/core'; import {hasOwnProperty} from '../Utils/utils'; import {PluginOptions} from './Options'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts index a221b0485c3dc..5f6c6986e04fd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {NodePath} from '@babel/core'; import * as t from '@babel/types'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidBlockNesting.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidBlockNesting.ts index cd2024e1e1069..adfb0510582cd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidBlockNesting.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidBlockNesting.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {CompilerError} from '..'; import { BlockId, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts index 7c1fb54ea8058..6f69af4b4f2cd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {CompilerError} from '../CompilerError'; import {getScopes, recursivelyTraverseItems} from './AssertValidBlockNesting'; import {Environment} from './Environment'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts index cb6854d1b3674..e29ef51ce0806 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {CompilerError} from '../CompilerError'; import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables'; import {printDependency} from '../ReactiveScopes/PrintReactiveFunction'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts index 02a28b9f35776..cb787d04d0623 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {CompilerError} from '..'; import {assertNonNull} from './CollectHoistablePropertyLoads'; import { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts index 5b286e917d0d3..96d20ea644265 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import { HIRFunction, InstructionId, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts index e12d10b40612b..934fd98f73daf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import { ScopeId, HIRFunction, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PruneUnusedLabelsHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PruneUnusedLabelsHIR.ts index bf10e9f1f36dd..f0c488c3e1ec7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PruneUnusedLabelsHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PruneUnusedLabelsHIR.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {CompilerError} from '..'; import {BlockId, GotoVariant, HIRFunction} from './HIR'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts index 0d27ac7ca0f69..a70f49dacd13a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts @@ -1,3 +1,11 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as t from '@babel/types'; import {CompilerError, SourceLocation} from '..'; import { ArrayExpression, @@ -188,6 +196,7 @@ export function inferEffectDependencies(fn: HIRFunction): void { * the `infer-effect-deps/pruned-nonreactive-obj` fixture for an * explanation. */ + const usedDeps = []; for (const dep of scopeInfo.deps) { if ( ((isUseRefType(dep.identifier) || @@ -207,6 +216,23 @@ export function inferEffectDependencies(fn: HIRFunction): void { ); newInstructions.push(...instructions); effectDeps.push(place); + usedDeps.push(dep); + } + + // For LSP autodeps feature. + const decorations: Array = []; + for (const loc of collectDepUsages(usedDeps, fnExpr.value)) { + if (typeof loc === 'symbol') { + continue; + } + decorations.push(loc); + } + if (typeof value.loc !== 'symbol') { + fn.env.logger?.logEvent(fn.env.filename, { + kind: 'AutoDepsDecorations', + fnLoc: value.loc, + decorations, + }); } newInstructions.push({ @@ -232,6 +258,31 @@ export function inferEffectDependencies(fn: HIRFunction): void { rewriteInstrs.set(instr.id, newInstructions); fn.env.inferredEffectLocations.add(callee.loc); } + } else if ( + value.args.length >= 2 && + value.args.length - 1 === autodepFnLoads.get(callee.identifier.id) && + value.args[0] != null && + value.args[0].kind === 'Identifier' + ) { + const penultimateArg = value.args[value.args.length - 2]; + const depArrayArg = value.args[value.args.length - 1]; + if ( + depArrayArg.kind !== 'Spread' && + penultimateArg.kind !== 'Spread' && + typeof depArrayArg.loc !== 'symbol' && + typeof penultimateArg.loc !== 'symbol' && + typeof value.loc !== 'symbol' + ) { + fn.env.logger?.logEvent(fn.env.filename, { + kind: 'AutoDepsEligible', + fnLoc: value.loc, + depArrayLoc: { + ...depArrayArg.loc, + start: penultimateArg.loc.end, + end: depArrayArg.loc.end, + }, + }); + } } } } @@ -340,3 +391,34 @@ function inferReactiveIdentifiers(fn: HIRFunction): Set { } return reactiveIds; } + +function collectDepUsages( + deps: Array, + fnExpr: FunctionExpression, +): Array { + const identifiers: Map = new Map(); + const loadedDeps: Set = new Set(); + const sourceLocations = []; + for (const dep of deps) { + identifiers.set(dep.identifier.id, dep); + } + + for (const [, block] of fnExpr.loweredFunc.func.body.blocks) { + for (const instr of block.instructions) { + if ( + instr.value.kind === 'LoadLocal' && + identifiers.has(instr.value.place.identifier.id) + ) { + loadedDeps.add(instr.lvalue.identifier.id); + } + for (const place of eachInstructionOperand(instr)) { + if (loadedDeps.has(place.identifier.id)) { + // TODO(@jbrown215): handle member exprs!! + sourceLocations.push(place.identifier.loc); + } + } + } + } + + return sourceLocations; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/StabilizeBlockIds.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/StabilizeBlockIds.ts index 26d83f9906443..9ad181547469a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/StabilizeBlockIds.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/StabilizeBlockIds.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import { BlockId, ReactiveFunction, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts index 8665ead0b1af0..4f142104f210b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts @@ -4,4 +4,5 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ + export {transformFire} from './TransformFire'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts index 4ba70b64a8c94..8989cb1ac2d62 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts @@ -4,6 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ + import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..'; import {HIRFunction, IdentifierId} from '../HIR'; import {DEFAULT_GLOBALS} from '../HIR/Globals'; diff --git a/compiler/packages/babel-plugin-react-compiler/tsup.config.ts b/compiler/packages/babel-plugin-react-compiler/tsup.config.ts index 12c04ec8a2480..dde9525256db6 100644 --- a/compiler/packages/babel-plugin-react-compiler/tsup.config.ts +++ b/compiler/packages/babel-plugin-react-compiler/tsup.config.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {defineConfig} from 'tsup'; export default defineConfig({ diff --git a/compiler/packages/eslint-plugin-react-compiler/tsup.config.ts b/compiler/packages/eslint-plugin-react-compiler/tsup.config.ts index ac13d33bba5b3..3e3b1b13131a5 100644 --- a/compiler/packages/eslint-plugin-react-compiler/tsup.config.ts +++ b/compiler/packages/eslint-plugin-react-compiler/tsup.config.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {defineConfig} from 'tsup'; export default defineConfig({ diff --git a/compiler/packages/make-read-only-util/tsup.config.ts b/compiler/packages/make-read-only-util/tsup.config.ts index cb65a61aaa319..ffef80abba260 100644 --- a/compiler/packages/make-read-only-util/tsup.config.ts +++ b/compiler/packages/make-read-only-util/tsup.config.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {defineConfig} from 'tsup'; export default defineConfig({ diff --git a/compiler/packages/react-compiler-healthcheck/src/config.ts b/compiler/packages/react-compiler-healthcheck/src/config.ts index 9655d2654b8b9..f279bc5d22062 100644 --- a/compiler/packages/react-compiler-healthcheck/src/config.ts +++ b/compiler/packages/react-compiler-healthcheck/src/config.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + export const config = { knownIncompatibleLibraries: [ 'mobx-react', diff --git a/compiler/packages/react-compiler-healthcheck/tsup.config.ts b/compiler/packages/react-compiler-healthcheck/tsup.config.ts index 9fe1e493dbf18..7addc79bf909a 100644 --- a/compiler/packages/react-compiler-healthcheck/tsup.config.ts +++ b/compiler/packages/react-compiler-healthcheck/tsup.config.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {defineConfig} from 'tsup'; export default defineConfig({ diff --git a/compiler/packages/react-compiler-runtime/tsup.config.ts b/compiler/packages/react-compiler-runtime/tsup.config.ts index ebc8df6f14e67..30a7f9da96f0d 100644 --- a/compiler/packages/react-compiler-runtime/tsup.config.ts +++ b/compiler/packages/react-compiler-runtime/tsup.config.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {defineConfig} from 'tsup'; export default defineConfig({ diff --git a/compiler/packages/react-forgive/client/src/autodeps.ts b/compiler/packages/react-forgive/client/src/autodeps.ts new file mode 100644 index 0000000000000..ed41b34dcb7d8 --- /dev/null +++ b/compiler/packages/react-forgive/client/src/autodeps.ts @@ -0,0 +1,106 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as vscode from 'vscode'; +import { + LanguageClient, + RequestType, + type Position, +} from 'vscode-languageclient/node'; +import {positionLiteralToVSCodePosition, positionsToRange} from './mapping'; + +export type AutoDepsDecorationsLSPEvent = { + useEffectCallExpr: [Position, Position]; + decorations: Array<[Position, Position]>; +}; + +export interface AutoDepsDecorationsParams { + position: Position; +} + +export namespace AutoDepsDecorationsRequest { + export const type = new RequestType< + AutoDepsDecorationsParams, + AutoDepsDecorationsLSPEvent | null, + void + >('react/autodeps_decorations'); +} + +const inferredEffectDepDecoration = + vscode.window.createTextEditorDecorationType({ + // TODO: make configurable? + borderColor: new vscode.ThemeColor('diffEditor.move.border'), + borderStyle: 'solid', + borderWidth: '0 0 4px 0', + }); + +let currentlyDecoratedAutoDepFnLoc: vscode.Range | null = null; +export function getCurrentlyDecoratedAutoDepFnLoc(): vscode.Range | null { + return currentlyDecoratedAutoDepFnLoc; +} +export function setCurrentlyDecoratedAutoDepFnLoc(range: vscode.Range): void { + currentlyDecoratedAutoDepFnLoc = range; +} +export function clearCurrentlyDecoratedAutoDepFnLoc(): void { + currentlyDecoratedAutoDepFnLoc = null; +} + +let decorationRequestId = 0; +export type AutoDepsDecorationsOptions = { + shouldUpdateCurrent: boolean; +}; +export function requestAutoDepsDecorations( + client: LanguageClient, + position: vscode.Position, + options: AutoDepsDecorationsOptions, +) { + const id = ++decorationRequestId; + client + .sendRequest(AutoDepsDecorationsRequest.type, {position}) + .then(response => { + if (response !== null) { + const { + decorations, + useEffectCallExpr: [start, end], + } = response; + // Maintain ordering + if (decorationRequestId === id) { + if (options.shouldUpdateCurrent) { + setCurrentlyDecoratedAutoDepFnLoc(positionsToRange(start, end)); + } + drawInferredEffectDepDecorations(decorations); + } + } else { + clearCurrentlyDecoratedAutoDepFnLoc(); + clearDecorations(inferredEffectDepDecoration); + } + }); +} + +export function drawInferredEffectDepDecorations( + decorations: Array<[Position, Position]>, +): void { + const decorationOptions = decorations.map(([start, end]) => { + return { + range: new vscode.Range( + positionLiteralToVSCodePosition(start), + positionLiteralToVSCodePosition(end), + ), + hoverMessage: 'Inferred as an effect dependency', + }; + }); + vscode.window.activeTextEditor?.setDecorations( + inferredEffectDepDecoration, + decorationOptions, + ); +} + +export function clearDecorations( + decorationType: vscode.TextEditorDecorationType, +) { + vscode.window.activeTextEditor?.setDecorations(decorationType, []); +} diff --git a/compiler/packages/react-forgive/client/src/colors.ts b/compiler/packages/react-forgive/client/src/colors.ts new file mode 100644 index 0000000000000..8989aa1c62ce9 --- /dev/null +++ b/compiler/packages/react-forgive/client/src/colors.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +type RGB = [number, number, number]; + +const int = Math.floor; + +export class Color { + constructor( + private r: number, + private g: number, + private b: number, + ) {} + + toAlphaString(a: number) { + return this.toCssString(a); + } + toString() { + return this.toCssString(1); + } + + /** + * Adjust the color by a multiplier to lighten (`> 1.0`) or darken (`< 1.0`) the color. Returns a new + * instance. + */ + adjusted(mult: number) { + const adjusted = Color.redistribute([ + this.r * mult, + this.g * mult, + this.b * mult, + ]); + return new Color(...adjusted); + } + + private toCssString(a: number) { + return `rgba(${this.r},${this.g},${this.b},${a})`; + } + /** + * Redistributes rgb, maintaing hue until its clamped. + * https://stackoverflow.com/a/141943 + */ + private static redistribute([r, g, b]: RGB): RGB { + const threshold = 255.999; + const max = Math.max(r, g, b); + if (max <= threshold) { + return [int(r), int(g), int(b)]; + } + const total = r + g + b; + if (total >= 3 * threshold) { + return [int(threshold), int(threshold), int(threshold)]; + } + const x = (3 * threshold - total) / (3 * max - total); + const gray = threshold - x * max; + return [int(gray + x * r), int(gray + x * g), int(gray + x * b)]; + } +} + +export const BLACK = new Color(0, 0, 0); +export const WHITE = new Color(255, 255, 255); + +const COLOR_POOL = [ + new Color(249, 65, 68), + new Color(243, 114, 44), + new Color(248, 150, 30), + new Color(249, 132, 74), + new Color(249, 199, 79), + new Color(144, 190, 109), + new Color(67, 170, 139), + new Color(77, 144, 142), + new Color(87, 117, 144), + new Color(39, 125, 161), +]; + +export function getColorFor(index: number): Color { + return COLOR_POOL[Math.abs(index) % COLOR_POOL.length]!; +} diff --git a/compiler/packages/react-forgive/client/src/extension.ts b/compiler/packages/react-forgive/client/src/extension.ts index 402f298fd74c5..e9938c388a03c 100644 --- a/compiler/packages/react-forgive/client/src/extension.ts +++ b/compiler/packages/react-forgive/client/src/extension.ts @@ -1,17 +1,34 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import * as path from 'path'; -import {ExtensionContext, window as Window} from 'vscode'; +import * as vscode from 'vscode'; import { LanguageClient, LanguageClientOptions, + type Position, ServerOptions, TransportKind, } from 'vscode-languageclient/node'; +import {positionLiteralToVSCodePosition} from './mapping'; +import { + getCurrentlyDecoratedAutoDepFnLoc, + requestAutoDepsDecorations, +} from './autodeps'; let client: LanguageClient; -export function activate(context: ExtensionContext) { +export function activate(context: vscode.ExtensionContext) { const serverModule = context.asAbsolutePath(path.join('dist', 'server.js')); + const documentSelector = [ + {scheme: 'file', language: 'javascriptreact'}, + {scheme: 'file', language: 'typescriptreact'}, + ]; // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used @@ -27,10 +44,7 @@ export function activate(context: ExtensionContext) { }; const clientOptions: LanguageClientOptions = { - documentSelector: [ - {scheme: 'file', language: 'javascriptreact'}, - {scheme: 'file', language: 'typescriptreact'}, - ], + documentSelector, progressOnInitialization: true, }; @@ -43,12 +57,39 @@ export function activate(context: ExtensionContext) { clientOptions, ); } catch { - Window.showErrorMessage( + vscode.window.showErrorMessage( `React Analyzer couldn't be started. See the output channel for details.`, ); return; } + vscode.languages.registerHoverProvider(documentSelector, { + provideHover(_document, position, _token) { + requestAutoDepsDecorations(client, position, {shouldUpdateCurrent: true}); + return null; + }, + }); + + vscode.workspace.onDidChangeTextDocument(async _e => { + const currentlyDecoratedAutoDepFnLoc = getCurrentlyDecoratedAutoDepFnLoc(); + if (currentlyDecoratedAutoDepFnLoc !== null) { + requestAutoDepsDecorations(client, currentlyDecoratedAutoDepFnLoc.start, { + shouldUpdateCurrent: false, + }); + } + }); + + vscode.commands.registerCommand( + 'react.requestAutoDepsDecorations', + (position: Position) => { + requestAutoDepsDecorations( + client, + positionLiteralToVSCodePosition(position), + {shouldUpdateCurrent: true}, + ); + }, + ); + client.registerProposedFeatures(); client.start(); } @@ -57,4 +98,5 @@ export function deactivate(): Thenable | undefined { if (client !== undefined) { return client.stop(); } + return; } diff --git a/compiler/packages/react-forgive/client/src/mapping.ts b/compiler/packages/react-forgive/client/src/mapping.ts new file mode 100644 index 0000000000000..3ec01ef8f6288 --- /dev/null +++ b/compiler/packages/react-forgive/client/src/mapping.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as vscode from 'vscode'; +import {Position} from 'vscode-languageclient/node'; + +export function positionLiteralToVSCodePosition( + position: Position, +): vscode.Position { + return new vscode.Position(position.line, position.character); +} + +export function positionsToRange(start: Position, end: Position): vscode.Range { + return new vscode.Range( + positionLiteralToVSCodePosition(start), + positionLiteralToVSCodePosition(end), + ); +} diff --git a/compiler/packages/react-forgive/server/src/compiler/compat.ts b/compiler/packages/react-forgive/server/src/compiler/compat.ts index 8b13f1df886ea..10271cbdcdc32 100644 --- a/compiler/packages/react-forgive/server/src/compiler/compat.ts +++ b/compiler/packages/react-forgive/server/src/compiler/compat.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {SourceLocation} from 'babel-plugin-react-compiler/src'; import {type Range} from 'vscode-languageserver'; diff --git a/compiler/packages/react-forgive/server/src/index.ts b/compiler/packages/react-forgive/server/src/index.ts index 395969c5e06bf..0b43e9fcd2945 100644 --- a/compiler/packages/react-forgive/server/src/index.ts +++ b/compiler/packages/react-forgive/server/src/index.ts @@ -7,10 +7,14 @@ import {TextDocument} from 'vscode-languageserver-textdocument'; import { + CodeAction, + CodeActionKind, CodeLens, + Command, createConnection, type InitializeParams, type InitializeResult, + Position, ProposedFeatures, TextDocuments, TextDocumentSyncKind, @@ -19,11 +23,22 @@ import {compile, lastResult} from './compiler'; import {type PluginOptions} from 'babel-plugin-react-compiler/src'; import {resolveReactConfig} from './compiler/options'; import { - CompileSuccessEvent, + type CompileSuccessEvent, + type LoggerEvent, defaultOptions, - LoggerEvent, } from 'babel-plugin-react-compiler/src/Entrypoint/Options'; import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat'; +import { + type AutoDepsDecorationsLSPEvent, + AutoDepsDecorationsRequest, + mapCompilerEventToLSPEvent, +} from './requests/autodepsdecorations'; +import { + isPositionWithinRange, + isRangeWithinRange, + Range, + sourceLocationToRange, +} from './utils/range'; const SUPPORTED_LANGUAGE_IDS = new Set([ 'javascript', @@ -37,17 +52,68 @@ const documents = new TextDocuments(TextDocument); let compilerOptions: PluginOptions | null = null; let compiledFns: Set = new Set(); +let autoDepsDecorations: Array = []; +let codeActionEvents: Array = []; + +type CodeActionLSPEvent = { + title: string; + kind: CodeActionKind; + newText: string; + anchorRange: Range; + editRange: {start: Position; end: Position}; +}; connection.onInitialize((_params: InitializeParams) => { // TODO(@poteto) get config fr compilerOptions = resolveReactConfig('.') ?? defaultOptions; compilerOptions = { ...compilerOptions, + environment: { + ...compilerOptions.environment, + inferEffectDependencies: [ + { + function: { + importSpecifierName: 'useEffect', + source: 'react', + }, + numRequiredArgs: 1, + }, + { + function: { + importSpecifierName: 'useSpecialEffect', + source: 'shared-runtime', + }, + numRequiredArgs: 2, + }, + { + function: { + importSpecifierName: 'default', + source: 'useEffectWrapper', + }, + numRequiredArgs: 1, + }, + ], + }, logger: { logEvent(_filename: string | null, event: LoggerEvent) { + connection.console.info(`Received event: ${event.kind}`); + connection.console.debug(JSON.stringify(event, null, 2)); if (event.kind === 'CompileSuccess') { compiledFns.add(event); } + if (event.kind === 'AutoDepsDecorations') { + autoDepsDecorations.push(mapCompilerEventToLSPEvent(event)); + } + if (event.kind === 'AutoDepsEligible') { + const depArrayLoc = sourceLocationToRange(event.depArrayLoc); + codeActionEvents.push({ + title: 'Use React Compiler inferred dependency array', + kind: CodeActionKind.QuickFix, + newText: '', + anchorRange: sourceLocationToRange(event.fnLoc), + editRange: {start: depArrayLoc[0], end: depArrayLoc[1]}, + }); + } }, }, }; @@ -55,6 +121,7 @@ connection.onInitialize((_params: InitializeParams) => { capabilities: { textDocumentSync: TextDocumentSyncKind.Full, codeLensProvider: {resolveProvider: true}, + codeActionProvider: {resolveProvider: true}, }, }; return result; @@ -66,19 +133,25 @@ connection.onInitialized(() => { documents.onDidChangeContent(async event => { connection.console.info(`Changed: ${event.document.uri}`); - compiledFns.clear(); + resetState(); if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) { const text = event.document.getText(); - await compile({ - text, - file: event.document.uri, - options: compilerOptions, - }); + try { + await compile({ + text, + file: event.document.uri, + options: compilerOptions, + }); + } catch (err) { + if (err instanceof Error) { + connection.console.error(err.stack ?? ''); + } + } } }); connection.onDidChangeWatchedFiles(change => { - compiledFns.clear(); + resetState(); connection.console.log( change.changes.map(c => `File changed: ${c.uri}`).join('\n'), ); @@ -118,6 +191,62 @@ connection.onCodeLensResolve(lens => { return lens; }); +connection.onCodeAction(params => { + connection.console.log('onCodeAction'); + const codeActions: Array = []; + for (const codeActionEvent of codeActionEvents) { + if ( + isRangeWithinRange( + [params.range.start, params.range.end], + codeActionEvent.anchorRange, + ) + ) { + const codeAction = CodeAction.create( + codeActionEvent.title, + { + changes: { + [params.textDocument.uri]: [ + { + newText: codeActionEvent.newText, + range: codeActionEvent.editRange, + }, + ], + }, + }, + codeActionEvent.kind, + ); + // After executing a codeaction, we want to draw autodep decorations again + codeAction.command = Command.create( + 'Request autodeps decorations', + 'react.requestAutoDepsDecorations', + codeActionEvent.anchorRange[0], + ); + codeActions.push(codeAction); + } + } + return codeActions; +}); + +/** + * The client can request the server to compute autodeps decorations based on a currently selected + * position if the selected position is within an autodep eligible function call. + */ +connection.onRequest(AutoDepsDecorationsRequest.type, async params => { + const position = params.position; + for (const decoration of autoDepsDecorations) { + if (isPositionWithinRange(position, decoration.useEffectCallExpr)) { + return decoration; + } + } + return null; +}); + +function resetState() { + compiledFns.clear(); + autoDepsDecorations = []; + codeActionEvents = []; +} + documents.listen(connection); connection.listen(); connection.console.info(`React Analyzer running in node ${process.version}`); diff --git a/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts b/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts new file mode 100644 index 0000000000000..77a568662e48c --- /dev/null +++ b/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler/src/Entrypoint'; +import {type Position} from 'vscode-languageserver-textdocument'; +import {RequestType} from 'vscode-languageserver/node'; +import {type Range, sourceLocationToRange} from '../utils/range'; + +export type AutoDepsDecorationsLSPEvent = { + useEffectCallExpr: Range; + decorations: Array; +}; +export interface AutoDepsDecorationsParams { + position: Position; +} +export namespace AutoDepsDecorationsRequest { + export const type = new RequestType< + AutoDepsDecorationsParams, + AutoDepsDecorationsLSPEvent, + void + >('react/autodeps_decorations'); +} + +export function mapCompilerEventToLSPEvent( + event: AutoDepsDecorationsEvent, +): AutoDepsDecorationsLSPEvent { + return { + useEffectCallExpr: sourceLocationToRange(event.fnLoc), + decorations: event.decorations.map(sourceLocationToRange), + }; +} diff --git a/compiler/packages/react-forgive/server/src/utils/range.ts b/compiler/packages/react-forgive/server/src/utils/range.ts new file mode 100644 index 0000000000000..5c164667c4f88 --- /dev/null +++ b/compiler/packages/react-forgive/server/src/utils/range.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as t from '@babel/types'; +import {type Position} from 'vscode-languageserver/node'; + +export type Range = [Position, Position]; + +export function isPositionWithinRange( + position: Position, + [start, end]: Range, +): boolean { + return position.line >= start.line && position.line <= end.line; +} + +export function isRangeWithinRange(aRange: Range, bRange: Range): boolean { + const startComparison = comparePositions(aRange[0], bRange[0]); + const endComparison = comparePositions(aRange[1], bRange[1]); + return startComparison >= 0 && endComparison <= 0; +} + +function comparePositions(a: Position, b: Position): number { + const lineComparison = a.line - b.line; + if (lineComparison === 0) { + return a.character - b.character; + } else { + return lineComparison; + } +} + +export function sourceLocationToRange( + loc: t.SourceLocation, +): [Position, Position] { + return [ + {line: loc.start.line - 1, character: loc.start.column}, + {line: loc.end.line - 1, character: loc.end.column}, + ]; +} diff --git a/compiler/packages/react-forgive/tsconfig.json b/compiler/packages/react-forgive/tsconfig.json new file mode 100644 index 0000000000000..6aeffaeb354da --- /dev/null +++ b/compiler/packages/react-forgive/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@tsconfig/strictest/tsconfig.json", + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16", + "rootDir": "../", + "noEmit": true, + "jsx": "react-jsxdev", + "lib": ["ES2022"], + + "target": "ES2022", + "importsNotUsedAsValues": "remove", + }, + "exclude": ["node_modules"], + "include": ["server/src/**/*.ts", "client/src/**/*.ts"], +} diff --git a/compiler/packages/react-mcp-server/src/types/algolia.ts b/compiler/packages/react-mcp-server/src/types/algolia.ts index 68914076a362c..1921dd3f718f1 100644 --- a/compiler/packages/react-mcp-server/src/types/algolia.ts +++ b/compiler/packages/react-mcp-server/src/types/algolia.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + // https://github.com/algolia/docsearch/blob/15ebcba606b281aa0dddc4ccb8feb19d396bf79e/packages/docsearch-react/src/types/DocSearchHit.ts type ContentType = | 'content' diff --git a/compiler/packages/react-mcp-server/tsup.config.ts b/compiler/packages/react-mcp-server/tsup.config.ts index eefc6ee0cee95..820410e20f651 100644 --- a/compiler/packages/react-mcp-server/tsup.config.ts +++ b/compiler/packages/react-mcp-server/tsup.config.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import {defineConfig} from 'tsup'; export default defineConfig({ diff --git a/compiler/scripts/copyright.js b/compiler/scripts/copyright.js index da3c39fe859af..0a5ef2e29ae30 100644 --- a/compiler/scripts/copyright.js +++ b/compiler/scripts/copyright.js @@ -51,6 +51,9 @@ if (hasErrors) { } function processFile(file) { + if (fs.lstatSync(file).isDirectory()) { + return; + } let source = fs.readFileSync(file, 'utf8'); if (source.indexOf(META_COPYRIGHT_COMMENT_BLOCK) === 0) { diff --git a/compiler/scripts/release/prompt-for-otp.js b/compiler/scripts/release/prompt-for-otp.js index e69e4f1604f81..3cb1e419c784e 100644 --- a/compiler/scripts/release/prompt-for-otp.js +++ b/compiler/scripts/release/prompt-for-otp.js @@ -1,4 +1,10 @@ #!/usr/bin/env node +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ const prompt = require('prompt-promise'); diff --git a/compiler/scripts/release/publish.js b/compiler/scripts/release/publish.js index d367e331e309f..99c47384251cc 100755 --- a/compiler/scripts/release/publish.js +++ b/compiler/scripts/release/publish.js @@ -1,4 +1,10 @@ #!/usr/bin/env node +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ 'use strict'; diff --git a/compiler/scripts/release/shared/build-packages.js b/compiler/scripts/release/shared/build-packages.js index 21c88e3aae11e..0a5ac1f5c2b50 100644 --- a/compiler/scripts/release/shared/build-packages.js +++ b/compiler/scripts/release/shared/build-packages.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + const ora = require('ora'); const {execHelper} = require('./utils'); diff --git a/compiler/scripts/release/shared/packages.js b/compiler/scripts/release/shared/packages.js index 533041d119665..39970bdde6c39 100644 --- a/compiler/scripts/release/shared/packages.js +++ b/compiler/scripts/release/shared/packages.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + const PUBLISHABLE_PACKAGES = [ 'babel-plugin-react-compiler', 'eslint-plugin-react-compiler', diff --git a/compiler/scripts/release/shared/utils.js b/compiler/scripts/release/shared/utils.js index 8406e563303ba..bda2d28c032ce 100644 --- a/compiler/scripts/release/shared/utils.js +++ b/compiler/scripts/release/shared/utils.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + const cp = require('child_process'); const util = require('util');