Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
a59c9fc6bf [compiler] More useMemo validation
Two additional validations for useMemo:
* Disallow reassigning to values declared outside the useMemo callback (always on)
* Disallow unused useMemo calls (part of the validateNoVoidUseMemo feature flag, which in turn is off by default)

We should probably enable this flag though!
2025-10-16 10:54:09 -07:00
65 changed files with 628 additions and 1839 deletions

View File

@@ -9,7 +9,7 @@ Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2)
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panels timeline in your browser developer tools
- [React Performance tracks](https://react.dev/reference/developer-tooling/react-performance-tracks) appear on the Performance panels timeline in your browser developer tools
### New React DOM Features

View File

@@ -988,7 +988,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'void-use-memo',
description:
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
'Validates that useMemos always return a value. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
preset: LintRulePreset.RecommendedLatest,
};
}

View File

@@ -83,11 +83,21 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
export const USE_FIRE_FUNCTION_NAME = 'useFire';
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
export const MacroSchema = z.string();
export const MacroMethodSchema = z.union([
z.object({type: z.literal('wildcard')}),
z.object({type: z.literal('name'), name: z.string()}),
]);
// Would like to change this to drop the string option, but breaks compatibility with existing configs
export const MacroSchema = z.union([
z.string(),
z.tuple([z.string(), z.array(MacroMethodSchema)]),
]);
export type CompilerMode = 'all_features' | 'no_inferred_memo';
export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
const HookSchema = z.object({
/*
@@ -649,7 +659,7 @@ export const EnvironmentConfigSchema = z.object({
* Invalid:
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(true),
validateNoVoidUseMemo: z.boolean().default(false),
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope

View File

@@ -438,6 +438,40 @@ export function dropManualMemoization(
continue;
}
/**
* Bailout on void return useMemos. This is an anti-pattern where code might be using
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
* values.
*/
if (
func.env.config.validateNoVoidUseMemo &&
manualMemo.kind === 'useMemo'
) {
const funcToCheck = sidemap.functions.get(
fnPlace.identifier.id,
)?.value;
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() callbacks must return a value',
description: `This ${
manualMemo.loadInstr.value.kind === 'PropertyLoad'
? 'React.useMemo()'
: 'useMemo()'
} callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message: 'useMemo() callbacks must return a value',
}),
);
}
}
}
instr.value = getManualMemoizationReplacement(
fnPlace,
instr.value.loc,
@@ -595,3 +629,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
}
return optionals;
}
function hasNonVoidReturn(func: HIRFunction): boolean {
for (const [, block] of func.body.blocks) {
if (block.terminal.kind === 'return') {
if (
block.terminal.returnVariant === 'Explicit' ||
block.terminal.returnVariant === 'Implicit'
) {
return true;
}
}
}
return false;
}

View File

@@ -19,7 +19,6 @@ import {
Environment,
FunctionExpression,
GeneratedSource,
getHookKind,
HIRFunction,
Hole,
IdentifierId,
@@ -199,7 +198,6 @@ export function inferMutationAliasingEffects(
isFunctionExpression,
fn,
hoistedContextDeclarations,
findNonMutatedDestructureSpreads(fn),
);
let iterationCount = 0;
@@ -289,18 +287,15 @@ class Context {
isFuctionExpression: boolean;
fn: HIRFunction;
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
nonMutatingSpreads: Set<IdentifierId>;
constructor(
isFunctionExpression: boolean,
fn: HIRFunction,
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
nonMutatingSpreads: Set<IdentifierId>,
) {
this.isFuctionExpression = isFunctionExpression;
this.fn = fn;
this.hoistedContextDeclarations = hoistedContextDeclarations;
this.nonMutatingSpreads = nonMutatingSpreads;
}
cacheApplySignature(
@@ -327,161 +322,6 @@ class Context {
}
}
/**
* Finds objects created via ObjectPattern spread destructuring
* (`const {x, ...spread} = ...`) where a) the rvalue is known frozen and
* b) the spread value cannot possibly be directly mutated. The idea is that
* for this set of values, we can treat the spread object as frozen.
*
* The primary use case for this is props spreading:
*
* ```
* function Component({prop, ...otherProps}) {
* const transformedProp = transform(prop, otherProps.foo);
* // pass `otherProps` down:
* return <Foo {...otherProps} prop={transformedProp} />;
* }
* ```
*
* Here we know that since `otherProps` cannot be mutated, we don't have to treat
* it as mutable: `otherProps.foo` only reads a value that must be frozen, so it
* can be treated as frozen too.
*/
function findNonMutatedDestructureSpreads(fn: HIRFunction): Set<IdentifierId> {
const knownFrozen = new Set<IdentifierId>();
if (fn.fnType === 'Component') {
const [props] = fn.params;
if (props != null && props.kind === 'Identifier') {
knownFrozen.add(props.identifier.id);
}
} else {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
knownFrozen.add(param.identifier.id);
}
}
}
// Map of temporaries to identifiers for spread objects
const candidateNonMutatingSpreads = new Map<IdentifierId, IdentifierId>();
for (const block of fn.body.blocks.values()) {
if (candidateNonMutatingSpreads.size !== 0) {
for (const phi of block.phis) {
for (const operand of phi.operands.values()) {
const spread = candidateNonMutatingSpreads.get(operand.identifier.id);
if (spread != null) {
candidateNonMutatingSpreads.delete(spread);
}
}
}
}
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'Destructure': {
if (
!knownFrozen.has(value.value.identifier.id) ||
!(
value.lvalue.kind === InstructionKind.Let ||
value.lvalue.kind === InstructionKind.Const
) ||
value.lvalue.pattern.kind !== 'ObjectPattern'
) {
continue;
}
for (const item of value.lvalue.pattern.properties) {
if (item.kind !== 'Spread') {
continue;
}
candidateNonMutatingSpreads.set(
item.place.identifier.id,
item.place.identifier.id,
);
}
break;
}
case 'LoadLocal': {
const spread = candidateNonMutatingSpreads.get(
value.place.identifier.id,
);
if (spread != null) {
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
}
break;
}
case 'StoreLocal': {
const spread = candidateNonMutatingSpreads.get(
value.value.identifier.id,
);
if (spread != null) {
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
candidateNonMutatingSpreads.set(
value.lvalue.place.identifier.id,
spread,
);
}
break;
}
case 'JsxFragment':
case 'JsxExpression': {
// Passing objects created with spread to jsx can't mutate them
break;
}
case 'PropertyLoad': {
// Properties must be frozen since the original value was frozen
break;
}
case 'CallExpression':
case 'MethodCall': {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (getHookKind(fn.env, callee.identifier) != null) {
// Hook calls have frozen arguments, and non-ref returns are frozen
if (!isRefOrRefValue(lvalue.identifier)) {
knownFrozen.add(lvalue.identifier.id);
}
} else {
// Non-hook calls check their operands, since they are potentially mutable
if (candidateNonMutatingSpreads.size !== 0) {
// Otherwise any reference to the spread object itself may mutate
for (const operand of eachInstructionValueOperand(value)) {
const spread = candidateNonMutatingSpreads.get(
operand.identifier.id,
);
if (spread != null) {
candidateNonMutatingSpreads.delete(spread);
}
}
}
}
break;
}
default: {
if (candidateNonMutatingSpreads.size !== 0) {
// Otherwise any reference to the spread object itself may mutate
for (const operand of eachInstructionValueOperand(value)) {
const spread = candidateNonMutatingSpreads.get(
operand.identifier.id,
);
if (spread != null) {
candidateNonMutatingSpreads.delete(spread);
}
}
}
}
}
}
}
const nonMutatingSpreads = new Set<IdentifierId>();
for (const [key, value] of candidateNonMutatingSpreads) {
if (key === value) {
nonMutatingSpreads.add(key);
}
}
return nonMutatingSpreads;
}
function inferParam(
param: Place | SpreadPattern,
initialState: InferenceState,
@@ -2214,9 +2054,7 @@ function computeSignatureForInstruction(
kind: 'Create',
into: place,
reason: ValueReason.Other,
value: context.nonMutatingSpreads.has(place.identifier.id)
? ValueKind.Frozen
: ValueKind.Mutable,
value: ValueKind.Mutable,
});
effects.push({
kind: 'Capture',

View File

@@ -7,6 +7,7 @@
import {
HIRFunction,
Identifier,
IdentifierId,
InstructionValue,
makeInstructionId,
@@ -14,35 +15,9 @@ import {
Place,
ReactiveScope,
} from '../HIR';
import {Macro} from '../HIR/Environment';
import {Macro, MacroMethod} from '../HIR/Environment';
import {eachInstructionValueOperand} from '../HIR/visitors';
/**
* Whether a macro requires its arguments to be transitively inlined (eg fbt)
* or just avoid having the top-level values be converted to variables (eg fbt.param)
*/
enum InlineLevel {
Transitive = 'Transitive',
Shallow = 'Shallow',
}
type MacroDefinition = {
level: InlineLevel;
properties: Map<string, MacroDefinition> | null;
};
const SHALLOW_MACRO: MacroDefinition = {
level: InlineLevel.Shallow,
properties: null,
};
const TRANSITIVE_MACRO: MacroDefinition = {
level: InlineLevel.Transitive,
properties: null,
};
const FBT_MACRO: MacroDefinition = {
level: InlineLevel.Transitive,
properties: new Map([['*', SHALLOW_MACRO]]),
};
FBT_MACRO.properties!.set('enum', FBT_MACRO);
import {Iterable_some} from '../Utils/utils';
/**
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
@@ -67,210 +42,250 @@ FBT_MACRO.properties!.set('enum', FBT_MACRO);
* ## User-defined macro-like function
*
* Users can also specify their own functions to be treated similarly to fbt via the
* `customMacros` environment configuration. By default, user-supplied custom macros
* have their arguments transitively inlined.
* `customMacros` environment configuration.
*/
export function memoizeFbtAndMacroOperandsInSameScope(
fn: HIRFunction,
): Set<IdentifierId> {
const macroKinds = new Map<Macro, MacroDefinition>([
...Array.from(FBT_TAGS.entries()),
...(fn.env.config.customMacros ?? []).map(
name => [name, TRANSITIVE_MACRO] as [Macro, MacroDefinition],
),
const fbtMacroTags = new Set<Macro>([
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
...(fn.env.config.customMacros ?? []),
]);
/**
* Forward data-flow analysis to identify all macro tags, including
* things like `fbt.foo.bar(...)`
* Set of all identifiers that load fbt or other macro functions or their nested
* properties, as well as values known to be the results of invoking macros
*/
const macroTags = populateMacroTags(fn, macroKinds);
const macroTagsCalls: Set<IdentifierId> = new Set();
/**
* Reverse data-flow analysis to merge arguments to macro *invocations*
* based on the kind of the macro
* Mapping of lvalue => list of operands for all expressions where either
* the lvalue is a known fbt/macro call and/or the operands transitively
* contain fbt/macro calls.
*
* This is the key data structure that powers the scope merging: we start
* at the lvalues and merge operands into the lvalue's scope.
*/
const macroValues = mergeMacroArguments(fn, macroTags, macroKinds);
const macroValues: Map<Identifier, Array<Identifier>> = new Map();
// Tracks methods loaded from macros, like fbt.param or idx.foo
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
return macroValues;
visit(fn, fbtMacroTags, macroTagsCalls, macroMethods, macroValues);
for (const root of macroValues.keys()) {
const scope = root.scope;
if (scope == null) {
continue;
}
// Merge the operands into the same scope if this is a known macro invocation
if (!macroTagsCalls.has(root.id)) {
continue;
}
mergeScopes(root, scope, macroValues, macroTagsCalls);
}
return macroTagsCalls;
}
const FBT_TAGS: Map<string, MacroDefinition> = new Map([
['fbt', FBT_MACRO],
['fbt:param', SHALLOW_MACRO],
['fbt:enum', FBT_MACRO],
['fbt:plural', SHALLOW_MACRO],
['fbs', FBT_MACRO],
['fbs:param', SHALLOW_MACRO],
['fbs:enum', FBT_MACRO],
['fbs:plural', SHALLOW_MACRO],
export const FBT_TAGS: Set<string> = new Set([
'fbt',
'fbt:param',
'fbt:enum',
'fbt:plural',
'fbs',
'fbs:param',
'fbs:enum',
'fbs:plural',
]);
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
'fbt:param',
'fbs:param',
]);
function populateMacroTags(
function visit(
fn: HIRFunction,
macroKinds: Map<Macro, MacroDefinition>,
): Map<IdentifierId, MacroDefinition> {
const macroTags = new Map<IdentifierId, MacroDefinition>();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'Primitive': {
if (typeof value.value === 'string') {
const macroDefinition = macroKinds.get(value.value);
if (macroDefinition != null) {
/*
* We don't distinguish between tag names and strings, so record
* all `fbt` string literals in case they are used as a jsx tag.
*/
macroTags.set(lvalue.identifier.id, macroDefinition);
fbtMacroTags: Set<Macro>,
macroTagsCalls: Set<IdentifierId>,
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
macroValues: Map<Identifier, Array<Identifier>>,
): void {
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
const macroOperands: Array<Identifier> = [];
for (const operand of phi.operands.values()) {
if (macroValues.has(operand.identifier)) {
macroOperands.push(operand.identifier);
}
}
if (macroOperands.length !== 0) {
macroValues.set(phi.place.identifier, macroOperands);
}
}
for (const instruction of block.instructions) {
const {lvalue, value} = instruction;
if (lvalue === null) {
continue;
}
if (
value.kind === 'Primitive' &&
typeof value.value === 'string' &&
matchesExactTag(value.value, fbtMacroTags)
) {
/*
* We don't distinguish between tag names and strings, so record
* all `fbt` string literals in case they are used as a jsx tag.
*/
macroTagsCalls.add(lvalue.identifier.id);
} else if (
value.kind === 'LoadGlobal' &&
matchesExactTag(value.binding.name, fbtMacroTags)
) {
// Record references to `fbt` as a global
macroTagsCalls.add(lvalue.identifier.id);
} else if (
value.kind === 'LoadGlobal' &&
matchTagRoot(value.binding.name, fbtMacroTags) !== null
) {
const methods = matchTagRoot(value.binding.name, fbtMacroTags)!;
macroMethods.set(lvalue.identifier.id, methods);
} else if (
value.kind === 'PropertyLoad' &&
macroMethods.has(value.object.identifier.id)
) {
const methods = macroMethods.get(value.object.identifier.id)!;
const newMethods = [];
for (const method of methods) {
if (
method.length > 0 &&
(method[0].type === 'wildcard' ||
(method[0].type === 'name' && method[0].name === value.property))
) {
if (method.length > 1) {
newMethods.push(method.slice(1));
} else {
macroTagsCalls.add(lvalue.identifier.id);
}
}
break;
}
case 'LoadGlobal': {
let macroDefinition = macroKinds.get(value.binding.name);
if (macroDefinition != null) {
macroTags.set(lvalue.identifier.id, macroDefinition);
if (newMethods.length > 0) {
macroMethods.set(lvalue.identifier.id, newMethods);
}
} else if (
value.kind === 'PropertyLoad' &&
macroTagsCalls.has(value.object.identifier.id)
) {
macroTagsCalls.add(lvalue.identifier.id);
} else if (
isFbtJsxExpression(fbtMacroTags, macroTagsCalls, value) ||
isFbtJsxChild(macroTagsCalls, lvalue, value) ||
isFbtCallExpression(macroTagsCalls, value)
) {
macroTagsCalls.add(lvalue.identifier.id);
macroValues.set(
lvalue.identifier,
Array.from(
eachInstructionValueOperand(value),
operand => operand.identifier,
),
);
} else if (
Iterable_some(eachInstructionValueOperand(value), operand =>
macroValues.has(operand.identifier),
)
) {
const macroOperands: Array<Identifier> = [];
for (const operand of eachInstructionValueOperand(value)) {
if (macroValues.has(operand.identifier)) {
macroOperands.push(operand.identifier);
}
break;
}
case 'PropertyLoad': {
if (typeof value.property === 'string') {
const macroDefinition = macroTags.get(value.object.identifier.id);
if (macroDefinition != null) {
const propertyDefinition =
macroDefinition.properties != null
? (macroDefinition.properties.get(value.property) ??
macroDefinition.properties.get('*'))
: null;
const propertyMacro = propertyDefinition ?? macroDefinition;
macroTags.set(lvalue.identifier.id, propertyMacro);
}
}
break;
}
macroValues.set(lvalue.identifier, macroOperands);
}
}
}
return macroTags;
}
function mergeMacroArguments(
fn: HIRFunction,
macroTags: Map<IdentifierId, MacroDefinition>,
macroKinds: Map<Macro, MacroDefinition>,
): Set<IdentifierId> {
const macroValues = new Set<IdentifierId>(macroTags.keys());
for (const block of Array.from(fn.body.blocks.values()).reverse()) {
for (let i = block.instructions.length - 1; i >= 0; i--) {
const instr = block.instructions[i]!;
const {lvalue, value} = instr;
switch (value.kind) {
case 'DeclareContext':
case 'DeclareLocal':
case 'Destructure':
case 'LoadContext':
case 'LoadLocal':
case 'PostfixUpdate':
case 'PrefixUpdate':
case 'StoreContext':
case 'StoreLocal': {
// Instructions that never need to be merged
break;
}
case 'CallExpression':
case 'MethodCall': {
const scope = lvalue.identifier.scope;
if (scope == null) {
continue;
}
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const macroDefinition =
macroTags.get(callee.identifier.id) ??
macroTags.get(lvalue.identifier.id);
if (macroDefinition != null) {
visitOperands(
macroDefinition,
scope,
lvalue,
value,
macroValues,
macroTags,
);
}
break;
}
case 'JsxExpression': {
const scope = lvalue.identifier.scope;
if (scope == null) {
continue;
}
let macroDefinition;
if (value.tag.kind === 'Identifier') {
macroDefinition = macroTags.get(value.tag.identifier.id);
} else {
macroDefinition = macroKinds.get(value.tag.name);
}
macroDefinition ??= macroTags.get(lvalue.identifier.id);
if (macroDefinition != null) {
visitOperands(
macroDefinition,
scope,
lvalue,
value,
macroValues,
macroTags,
);
}
break;
}
default: {
const scope = lvalue.identifier.scope;
if (scope == null) {
continue;
}
const macroDefinition = macroTags.get(lvalue.identifier.id);
if (macroDefinition != null) {
visitOperands(
macroDefinition,
scope,
lvalue,
value,
macroValues,
macroTags,
);
}
break;
}
}
function mergeScopes(
root: Identifier,
scope: ReactiveScope,
macroValues: Map<Identifier, Array<Identifier>>,
macroTagsCalls: Set<IdentifierId>,
): void {
const operands = macroValues.get(root);
if (operands == null) {
return;
}
for (const operand of operands) {
operand.scope = scope;
expandFbtScopeRange(scope.range, operand.mutableRange);
macroTagsCalls.add(operand.id);
mergeScopes(operand, scope, macroValues, macroTagsCalls);
}
}
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
return Array.from(tags).some(macro =>
typeof macro === 'string'
? s === macro
: macro[1].length === 0 && macro[0] === s,
);
}
function matchTagRoot(
s: string,
tags: Set<Macro>,
): Array<Array<MacroMethod>> | null {
const methods: Array<Array<MacroMethod>> = [];
for (const macro of tags) {
if (typeof macro === 'string') {
continue;
}
for (const phi of block.phis) {
const scope = phi.place.identifier.scope;
if (scope == null) {
continue;
}
const macroDefinition = macroTags.get(phi.place.identifier.id);
if (
macroDefinition == null ||
macroDefinition.level === InlineLevel.Shallow
) {
continue;
}
macroValues.add(phi.place.identifier.id);
for (const operand of phi.operands.values()) {
operand.identifier.scope = scope;
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
macroTags.set(operand.identifier.id, macroDefinition);
macroValues.add(operand.identifier.id);
}
const [tag, rest] = macro;
if (tag === s && rest.length > 0) {
methods.push(rest);
}
}
return macroValues;
if (methods.length > 0) {
return methods;
} else {
return null;
}
}
function isFbtCallExpression(
macroTagsCalls: Set<IdentifierId>,
value: InstructionValue,
): boolean {
return (
(value.kind === 'CallExpression' &&
macroTagsCalls.has(value.callee.identifier.id)) ||
(value.kind === 'MethodCall' &&
macroTagsCalls.has(value.property.identifier.id))
);
}
function isFbtJsxExpression(
fbtMacroTags: Set<Macro>,
macroTagsCalls: Set<IdentifierId>,
value: InstructionValue,
): boolean {
return (
value.kind === 'JsxExpression' &&
((value.tag.kind === 'Identifier' &&
macroTagsCalls.has(value.tag.identifier.id)) ||
(value.tag.kind === 'BuiltinTag' &&
matchesExactTag(value.tag.name, fbtMacroTags)))
);
}
function isFbtJsxChild(
macroTagsCalls: Set<IdentifierId>,
lvalue: Place | null,
value: InstructionValue,
): boolean {
return (
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
lvalue !== null &&
macroTagsCalls.has(lvalue.identifier.id)
);
}
function expandFbtScopeRange(
@@ -283,22 +298,3 @@ function expandFbtScopeRange(
);
}
}
function visitOperands(
macroDefinition: MacroDefinition,
scope: ReactiveScope,
lvalue: Place,
value: InstructionValue,
macroValues: Set<IdentifierId>,
macroTags: Map<IdentifierId, MacroDefinition>,
): void {
macroValues.add(lvalue.identifier.id);
for (const operand of eachInstructionValueOperand(value)) {
if (macroDefinition.level === InlineLevel.Transitive) {
operand.identifier.scope = scope;
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
macroTags.set(operand.identifier.id, macroDefinition);
}
macroValues.add(operand.identifier.id);
}
}

View File

@@ -135,7 +135,16 @@ function parseConfigPragmaEnvironmentForTest(
} else if (val) {
const parsedVal = tryParseTestPragmaValue(val).unwrap();
if (key === 'customMacros' && typeof parsedVal === 'string') {
maybeConfig[key] = [parsedVal.split('.')[0]];
const valSplit = parsedVal.split('.');
const props = [];
for (const elt of valSplit.slice(1)) {
if (elt === '*') {
props.push({type: 'wildcard'});
} else if (elt.length > 0) {
props.push({type: 'name', name: elt});
}
}
maybeConfig[key] = [[valSplit[0], props]];
continue;
}
maybeConfig[key] = parsedVal;

View File

@@ -24,7 +24,6 @@ import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
const errors = new CompilerError();
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
const functions = new Map<IdentifierId, FunctionExpression>();
@@ -126,22 +125,7 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
if (fn.env.config.validateNoVoidUseMemo) {
if (!hasNonVoidReturn(body.loweredFunc.func)) {
voidMemoErrors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() callbacks must return a value',
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: body.loc,
message: 'useMemo() callbacks must return a value',
}),
);
} else {
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
}
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
}
break;
}
@@ -162,10 +146,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
*/
for (const loc of unusedUseMemos.values()) {
voidMemoErrors.pushDiagnostic(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() result is unused',
reason: 'Unused useMemo()',
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
@@ -176,7 +160,6 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
);
}
}
fn.env.logErrors(voidMemoErrors.asResult());
return errors.asResult();
}
@@ -184,45 +167,28 @@ function validateNoContextVariableAssignment(
fn: HIRFunction,
errors: CompilerError,
): void {
const context = new Set(fn.context.map(place => place.identifier.id));
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const value = instr.value;
switch (value.kind) {
case 'StoreContext': {
if (context.has(value.lvalue.place.identifier.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
'useMemo() callbacks may not reassign variables declared outside of the callback',
description:
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
suggestions: null,
}).withDetails({
kind: 'error',
loc: value.lvalue.place.loc,
message: 'Cannot reassign variable',
}),
);
}
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
'useMemo() callbacks may not reassign variables declared outside of the callback',
description:
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
suggestions: null,
}).withDetails({
kind: 'error',
loc: value.lvalue.place.loc,
message: 'Cannot reassign variable',
}),
);
break;
}
}
}
}
}
function hasNonVoidReturn(func: HIRFunction): boolean {
for (const [, block] of func.body.blocks) {
if (block.terminal.kind === 'return') {
if (
block.terminal.returnVariant === 'Explicit' ||
block.terminal.returnVariant === 'Implicit'
) {
return true;
}
}
}
return false;
}

View File

@@ -0,0 +1,35 @@
## Input
```javascript
// @validateNoVoidUseMemo
function Component() {
useMemo(() => {
return [];
}, []);
return <div />;
}
```
## Error
```
Found 1 error:
Error: Unused useMemo()
This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects.
error.invalid-unused-usememo.ts:3:2
1 | // @validateNoVoidUseMemo
2 | function Component() {
> 3 | useMemo(() => {
| ^^^^^^^ useMemo() result is unused
4 | return [];
5 | }, []);
6 | return <div />;
```

View File

@@ -1,4 +1,4 @@
// @validateNoVoidUseMemo @loggerTestOnly
// @validateNoVoidUseMemo
function Component() {
useMemo(() => {
return [];

View File

@@ -0,0 +1,64 @@
## Input
```javascript
// @validateNoVoidUseMemo
function Component() {
const value = useMemo(() => {
console.log('computing');
}, []);
const value2 = React.useMemo(() => {
console.log('computing');
}, []);
return (
<div>
{value}
{value2}
</div>
);
}
```
## Error
```
Found 2 errors:
Error: useMemo() callbacks must return a value
This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
error.useMemo-no-return-value.ts:3:16
1 | // @validateNoVoidUseMemo
2 | function Component() {
> 3 | const value = useMemo(() => {
| ^^^^^^^^^^^^^^^
> 4 | console.log('computing');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 5 | }, []);
| ^^^^^^^^^ useMemo() callbacks must return a value
6 | const value2 = React.useMemo(() => {
7 | console.log('computing');
8 | }, []);
Error: useMemo() callbacks must return a value
This React.useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
error.useMemo-no-return-value.ts:6:17
4 | console.log('computing');
5 | }, []);
> 6 | const value2 = React.useMemo(() => {
| ^^^^^^^^^^^^^^^^^^^^^
> 7 | console.log('computing');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 8 | }, []);
| ^^^^^^^^^ useMemo() callbacks must return a value
9 | return (
10 | <div>
11 | {value}
```

View File

@@ -1,4 +1,4 @@
// @validateNoVoidUseMemo @loggerTestOnly
// @validateNoVoidUseMemo
function Component() {
const value = useMemo(() => {
console.log('computing');

View File

@@ -44,23 +44,15 @@ import fbt from "fbt";
import { identity } from "shared-runtime";
function Component(props) {
const $ = _c(5);
const $ = _c(3);
let t0;
if ($[0] !== props.count || $[1] !== props.option) {
let t1;
if ($[3] !== props.count) {
t1 = identity(props.count);
$[3] = props.count;
$[4] = t1;
} else {
t1 = $[4];
}
t0 = (
<span>
{fbt._(
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
[
fbt._plural(t1, "count"),
fbt._plural(identity(props.count), "count"),
fbt._param(
"option",

View File

@@ -44,23 +44,15 @@ import fbt from "fbt";
import { identity } from "shared-runtime";
function Component(props) {
const $ = _c(5);
const $ = _c(3);
let t0;
if ($[0] !== props.count || $[1] !== props.option) {
let t1;
if ($[3] !== props.count) {
t1 = identity(props.count);
$[3] = props.count;
$[4] = t1;
} else {
t1 = $[4];
}
t0 = (
<span>
{fbt._(
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
[
fbt._plural(t1, "count"),
fbt._plural(identity(props.count), "count"),
fbt._param(
"option",

View File

@@ -37,7 +37,7 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Foo(t0) {
const $ = _c(13);
const $ = _c(7);
const { name1, name2 } = t0;
let t1;
if ($[0] !== name1 || $[1] !== name2) {
@@ -50,34 +50,19 @@ function Foo(t0) {
t2 = $[4];
}
let t3;
if ($[5] !== name1 || $[6] !== t2) {
t3 = <span key={name1}>{t2}</span>;
$[5] = name1;
$[6] = t2;
$[7] = t3;
if ($[5] !== name2) {
t3 = <b>{name2}</b>;
$[5] = name2;
$[6] = t3;
} else {
t3 = $[7];
}
let t4;
if ($[8] !== name2) {
t4 = <b>{name2}</b>;
$[8] = name2;
$[9] = t4;
} else {
t4 = $[9];
}
let t5;
if ($[10] !== name2 || $[11] !== t4) {
t5 = <span key={name2}>{t4}</span>;
$[10] = name2;
$[11] = t4;
$[12] = t5;
} else {
t5 = $[12];
t3 = $[6];
}
t1 = fbt._(
"{user1} and {user2} accepted your PR!",
[fbt._param("user1", t3), fbt._param("user2", t5)],
[
fbt._param("user1", <span key={name1}>{t2}</span>),
fbt._param("user2", <span key={name2}>{t3}</span>),
],
{ hk: "2PxMie" },
);
$[0] = name1;

View File

@@ -29,24 +29,20 @@ import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
function Component(t0) {
const $ = _c(6);
const $ = _c(4);
const { name, data, icon } = t0;
let t1;
if ($[0] !== data || $[1] !== icon || $[2] !== name) {
let t2;
if ($[4] !== name) {
t2 = <Text type="h4">{name}</Text>;
$[4] = name;
$[5] = t2;
} else {
t2 = $[5];
}
t1 = (
<Text type="body4">
{fbt._(
"{item author}{icon}{=m2}",
[
fbt._param("item author", t2),
fbt._param(
"item author",
<Text type="h4">{name}</Text>,
),
fbt._param(
"icon",

View File

@@ -27,21 +27,16 @@ import fbt from "fbt";
import { identity } from "shared-runtime";
function Component(props) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== props.text) {
const t1 = identity(props.text);
let t2;
if ($[2] !== t1) {
t2 = <>{t1}</>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
t0 = (
<Foo
value={fbt._("{value}%", [fbt._param("value", t2)], { hk: "10F5Cc" })}
value={fbt._(
"{value}%",
[fbt._param("value", <>{identity(props.text)}</>)],
{ hk: "10F5Cc" },
)}
/>
);
$[0] = props.text;

View File

@@ -1,109 +0,0 @@
## Input
```javascript
// @flow
import {fbt} from 'fbt';
function Example({x}) {
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
// be memoized separately
return (
<fbt desc="Description">
Outer Text
<Foo key="b" x={x}>
<Bar key="a">Inner Text</Bar>
</Foo>
</fbt>
);
}
function Foo({x, children}) {
'use no memo';
return (
<>
<div>{x}</div>
<span>{children}</span>
</>
);
}
function Bar({children}) {
'use no memo';
return children;
}
export const FIXTURE_ENTRYPOINT = {
fn: Example,
params: [{x: 'Hello'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { fbt } from "fbt";
function Example(t0) {
const $ = _c(2);
const { x } = t0;
let t1;
if ($[0] !== x) {
t1 = fbt._(
"Outer Text {=m1}",
[
fbt._implicitParam(
"=m1",
<Foo key="b" x={x}>
{fbt._(
"{=m1}",
[
fbt._implicitParam(
"=m1",
<Bar key="a">
{fbt._("Inner Text", null, { hk: "32YB0l" })}
</Bar>,
),
],
{ hk: "23dJsI" },
)}
</Foo>,
),
],
{ hk: "2RVA7V" },
);
$[0] = x;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
function Foo({ x, children }) {
"use no memo";
return (
<>
<div>{x}</div>
<span>{children}</span>
</>
);
}
function Bar({ children }) {
"use no memo";
return children;
}
export const FIXTURE_ENTRYPOINT = {
fn: Example,
params: [{ x: "Hello" }],
};
```
### Eval output
(kind: ok) Outer Text <div>Hello</div><span>Inner Text</span>

View File

@@ -1,35 +0,0 @@
// @flow
import {fbt} from 'fbt';
function Example({x}) {
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
// be memoized separately
return (
<fbt desc="Description">
Outer Text
<Foo key="b" x={x}>
<Bar key="a">Inner Text</Bar>
</Foo>
</fbt>
);
}
function Foo({x, children}) {
'use no memo';
return (
<>
<div>{x}</div>
<span>{children}</span>
</>
);
}
function Bar({children}) {
'use no memo';
return children;
}
export const FIXTURE_ENTRYPOINT = {
fn: Example,
params: [{x: 'Hello'}],
};

View File

@@ -1,128 +0,0 @@
## Input
```javascript
import fbt from 'fbt';
import {Stringify, identity} from 'shared-runtime';
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
* ---
* t3
* ---
*/
function Component({firstname, lastname}) {
'use memo';
return (
<div>
{fbt(
[
'Name: ',
fbt.param('firstname', <Stringify key={0} name={firstname} />),
', ',
fbt.param(
'lastname',
identity(
fbt(
'(inner)' +
fbt.param('lastname', <Stringify key={1} name={lastname} />),
'Inner fbt value'
)
)
),
],
'Name'
)}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstname: 'first', lastname: 'last'}],
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
import { Stringify, identity } from "shared-runtime";
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
* ---
* t3
* ---
*/
function Component(t0) {
"use memo";
const $ = _c(9);
const { firstname, lastname } = t0;
let t1;
if ($[0] !== firstname || $[1] !== lastname) {
let t2;
if ($[3] !== firstname) {
t2 = <Stringify key={0} name={firstname} />;
$[3] = firstname;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] !== lastname) {
t3 = <Stringify key={1} name={lastname} />;
$[5] = lastname;
$[6] = t3;
} else {
t3 = $[6];
}
t1 = fbt._(
"Name: {firstname}, {lastname}",
[
fbt._param("firstname", t2),
fbt._param(
"lastname",
identity(
fbt._("(inner){lastname}", [fbt._param("lastname", t3)], {
hk: "1Kdxyo",
}),
),
),
],
{ hk: "3AiIf8" },
);
$[0] = firstname;
$[1] = lastname;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[7] !== t1) {
t2 = <div>{t1}</div>;
$[7] = t1;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ firstname: "first", lastname: "last" }],
sequentialRenders: [{ firstname: "first", lastname: "last" }],
};
```
### Eval output
(kind: ok) <div>Name: <div>{"name":"first"}</div>, (inner)<div>{"name":"last"}</div></div>

View File

@@ -1,42 +0,0 @@
import fbt from 'fbt';
import {Stringify, identity} from 'shared-runtime';
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
* ---
* t3
* ---
*/
function Component({firstname, lastname}) {
'use memo';
return (
<div>
{fbt(
[
'Name: ',
fbt.param('firstname', <Stringify key={0} name={firstname} />),
', ',
fbt.param(
'lastname',
identity(
fbt(
'(inner)' +
fbt.param('lastname', <Stringify key={1} name={lastname} />),
'Inner fbt value'
)
)
),
],
'Name'
)}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstname: 'first', lastname: 'last'}],
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
};

View File

@@ -3,7 +3,7 @@
```javascript
import fbt from 'fbt';
import {identity} from 'shared-runtime';
import {Stringify} from 'shared-runtime';
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
@@ -16,25 +16,22 @@ import {identity} from 'shared-runtime';
function Component({firstname, lastname}) {
'use memo';
return (
<div>
<Stringify>
{fbt(
[
'Name: ',
fbt.param('firstname', identity(firstname)),
fbt.param('firstname', <Stringify key={0} name={firstname} />),
', ',
fbt.param(
'lastname',
identity(
fbt(
'(inner)' + fbt.param('lastname', identity(lastname)),
'Inner fbt value'
)
)
<Stringify key={0} name={lastname}>
{fbt('(inner fbt)', 'Inner fbt value')}
</Stringify>
),
],
'Name'
)}
</div>
</Stringify>
);
}
@@ -51,7 +48,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
import fbt from "fbt";
import { identity } from "shared-runtime";
import { Stringify } from "shared-runtime";
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
@@ -73,24 +70,14 @@ function Component(t0) {
fbt._param(
"firstname",
identity(firstname),
<Stringify key={0} name={firstname} />,
),
fbt._param(
"lastname",
identity(
fbt._(
"(inner){lastname}",
[
fbt._param(
"lastname",
identity(lastname),
),
],
{ hk: "1Kdxyo" },
),
),
<Stringify key={0} name={lastname}>
{fbt._("(inner fbt)", null, { hk: "36qNwF" })}
</Stringify>,
),
],
{ hk: "3AiIf8" },
@@ -103,7 +90,7 @@ function Component(t0) {
}
let t2;
if ($[3] !== t1) {
t2 = <div>{t1}</div>;
t2 = <Stringify>{t1}</Stringify>;
$[3] = t1;
$[4] = t2;
} else {
@@ -121,4 +108,4 @@ export const FIXTURE_ENTRYPOINT = {
```
### Eval output
(kind: ok) <div>Name: first, (inner)last</div>
(kind: ok) <div>{"children":"Name: , "}</div>

View File

@@ -1,5 +1,5 @@
import fbt from 'fbt';
import {identity} from 'shared-runtime';
import {Stringify} from 'shared-runtime';
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
@@ -12,25 +12,22 @@ import {identity} from 'shared-runtime';
function Component({firstname, lastname}) {
'use memo';
return (
<div>
<Stringify>
{fbt(
[
'Name: ',
fbt.param('firstname', identity(firstname)),
fbt.param('firstname', <Stringify key={0} name={firstname} />),
', ',
fbt.param(
'lastname',
identity(
fbt(
'(inner)' + fbt.param('lastname', identity(lastname)),
'Inner fbt value'
)
)
<Stringify key={0} name={lastname}>
{fbt('(inner fbt)', 'Inner fbt value')}
</Stringify>
),
],
'Name'
)}
</div>
</Stringify>
);
}

View File

@@ -37,7 +37,7 @@ function Component(props) {
const $ = _c(16);
let t0;
if ($[0] !== props) {
t0 = idx(props, (_) => _.group.label);
t0 = idx(props, _temp);
$[0] = props;
$[1] = t0;
} else {
@@ -46,7 +46,7 @@ function Component(props) {
const groupName1 = t0;
let t1;
if ($[2] !== props) {
t1 = idx.a(props, (__0) => __0.group.label);
t1 = idx.a(props, _temp2);
$[2] = props;
$[3] = t1;
} else {
@@ -108,6 +108,12 @@ function Component(props) {
}
return t5;
}
function _temp2(__0) {
return __0.group.label;
}
function _temp(_) {
return _.group.label;
}
```

View File

@@ -31,7 +31,7 @@ function Component(props) {
const $ = _c(10);
let t0;
if ($[0] !== props) {
t0 = idx(props, (_) => _.group.label);
t0 = idx(props, _temp);
$[0] = props;
$[1] = t0;
} else {
@@ -74,6 +74,9 @@ function Component(props) {
}
return t3;
}
function _temp(_) {
return _.group.label;
}
```

View File

@@ -1,41 +0,0 @@
## Input
```javascript
// @validateNoVoidUseMemo @loggerTestOnly
function Component() {
useMemo(() => {
return [];
}, []);
return <div />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
function Component() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() result is unused","description":"This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":2,"index":67},"end":{"line":3,"column":9,"index":74},"filename":"invalid-unused-usememo.ts","identifierName":"useMemo"},"message":"useMemo() result is unused"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":7,"column":1,"index":127},"filename":"invalid-unused-usememo.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,59 +0,0 @@
## Input
```javascript
// @validateNoVoidUseMemo @loggerTestOnly
function Component() {
const value = useMemo(() => {
console.log('computing');
}, []);
const value2 = React.useMemo(() => {
console.log('computing');
}, []);
return (
<div>
{value}
{value2}
</div>
);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
function Component() {
const $ = _c(1);
console.log("computing");
console.log("computing");
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (
<div>
{undefined}
{undefined}
</div>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":24,"index":89},"end":{"line":5,"column":3,"index":130},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":6,"column":31,"index":168},"end":{"line":8,"column":3,"index":209},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":15,"column":1,"index":283},"filename":"invalid-useMemo-no-return-value.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,33 +0,0 @@
## Input
```javascript
// @loggerTestOnly
function component(a) {
let x = useMemo(() => {
mutate(a);
}, []);
return x;
}
```
## Code
```javascript
// @loggerTestOnly
function component(a) {
mutate(a);
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":18,"index":61},"end":{"line":5,"column":3,"index":87},"filename":"invalid-useMemo-return-empty.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":19},"end":{"line":7,"column":1,"index":107},"filename":"invalid-useMemo-return-empty.ts"},"fnName":"component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":1,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,63 +0,0 @@
## Input
```javascript
import {identity, Stringify, useIdentity} from 'shared-runtime';
function Component(props) {
const {x, ...rest} = useIdentity(props);
const z = rest.z;
identity(z);
return <Stringify x={x} z={z} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 'Hello', z: 'World'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, Stringify, useIdentity } from "shared-runtime";
function Component(props) {
const $ = _c(6);
const t0 = useIdentity(props);
let rest;
let x;
if ($[0] !== t0) {
({ x, ...rest } = t0);
$[0] = t0;
$[1] = rest;
$[2] = x;
} else {
rest = $[1];
x = $[2];
}
const z = rest.z;
identity(z);
let t1;
if ($[3] !== x || $[4] !== z) {
t1 = <Stringify x={x} z={z} />;
$[3] = x;
$[4] = z;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ x: "Hello", z: "World" }],
};
```
### Eval output
(kind: ok) <div>{"x":"Hello","z":"World"}</div>

View File

@@ -1,13 +0,0 @@
import {identity, Stringify, useIdentity} from 'shared-runtime';
function Component(props) {
const {x, ...rest} = useIdentity(props);
const z = rest.z;
identity(z);
return <Stringify x={x} z={z} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 'Hello', z: 'World'}],
};

View File

@@ -1,57 +0,0 @@
## Input
```javascript
import {identity, Stringify} from 'shared-runtime';
function Component({x, ...rest}) {
return <Stringify {...rest} x={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 'Hello', z: 'World'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(6);
let rest;
let x;
if ($[0] !== t0) {
({ x, ...rest } = t0);
$[0] = t0;
$[1] = rest;
$[2] = x;
} else {
rest = $[1];
x = $[2];
}
let t1;
if ($[3] !== rest || $[4] !== x) {
t1 = <Stringify {...rest} x={x} />;
$[3] = rest;
$[4] = x;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ x: "Hello", z: "World" }],
};
```
### Eval output
(kind: ok) <div>{"z":"World","x":"Hello"}</div>

View File

@@ -1,10 +0,0 @@
import {identity, Stringify} from 'shared-runtime';
function Component({x, ...rest}) {
return <Stringify {...rest} x={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 'Hello', z: 'World'}],
};

View File

@@ -1,63 +0,0 @@
## Input
```javascript
import {identity, Stringify} from 'shared-runtime';
function Component({x, ...rest}) {
const restAlias = rest;
const z = restAlias.z;
identity(z);
return <Stringify x={x} z={z} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 'Hello', z: 'World'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(6);
let rest;
let x;
if ($[0] !== t0) {
({ x, ...rest } = t0);
$[0] = t0;
$[1] = rest;
$[2] = x;
} else {
rest = $[1];
x = $[2];
}
const restAlias = rest;
const z = restAlias.z;
identity(z);
let t1;
if ($[3] !== x || $[4] !== z) {
t1 = <Stringify x={x} z={z} />;
$[3] = x;
$[4] = z;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ x: "Hello", z: "World" }],
};
```
### Eval output
(kind: ok) <div>{"x":"Hello","z":"World"}</div>

View File

@@ -1,13 +0,0 @@
import {identity, Stringify} from 'shared-runtime';
function Component({x, ...rest}) {
const restAlias = rest;
const z = restAlias.z;
identity(z);
return <Stringify x={x} z={z} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 'Hello', z: 'World'}],
};

View File

@@ -1,61 +0,0 @@
## Input
```javascript
import {identity, Stringify} from 'shared-runtime';
function Component({x, ...rest}) {
const z = rest.z;
identity(z);
return <Stringify x={x} z={z} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 'Hello', z: 'World'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { identity, Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(6);
let rest;
let x;
if ($[0] !== t0) {
({ x, ...rest } = t0);
$[0] = t0;
$[1] = rest;
$[2] = x;
} else {
rest = $[1];
x = $[2];
}
const z = rest.z;
identity(z);
let t1;
if ($[3] !== x || $[4] !== z) {
t1 = <Stringify x={x} z={z} />;
$[3] = x;
$[4] = z;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ x: "Hello", z: "World" }],
};
```
### Eval output
(kind: ok) <div>{"x":"Hello","z":"World"}</div>

View File

@@ -1,12 +0,0 @@
import {identity, Stringify} from 'shared-runtime';
function Component({x, ...rest}) {
const z = rest.z;
identity(z);
return <Stringify x={x} z={z} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 'Hello', z: 'World'}],
};

View File

@@ -1,45 +0,0 @@
## Input
```javascript
// @flow
export hook useItemLanguage(items) {
return useMemo(() => {
let language: ?string = null;
items.forEach(item => {
if (item.language != null) {
language = item.language;
}
});
return language;
}, [items]);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
export function useItemLanguage(items) {
const $ = _c(2);
let language;
if ($[0] !== items) {
language = null;
items.forEach((item) => {
if (item.language != null) {
language = item.language;
}
});
$[0] = items;
$[1] = language;
} else {
language = $[1];
}
return language;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,12 +0,0 @@
// @flow
export hook useItemLanguage(items) {
return useMemo(() => {
let language: ?string = null;
items.forEach(item => {
if (item.language != null) {
language = item.language;
}
});
return language;
}, [items]);
}

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @validateNoVoidUseMemo:false
function Component(props) {
const item = props.item;
const thumbnails = [];
@@ -23,7 +22,7 @@ function Component(props) {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo:false
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(6);
const item = props.item;

View File

@@ -1,4 +1,3 @@
// @validateNoVoidUseMemo:false
function Component(props) {
const item = props.item;
const thumbnails = [];

View File

@@ -6,7 +6,6 @@ function Component(props) {
const x = useMemo(() => {
if (props.cond) {
if (props.cond) {
return props.value;
}
}
}, [props.cond]);
@@ -25,18 +24,10 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
function Component(props) {
let t0;
bb0: {
if (props.cond) {
if (props.cond) {
if (props.cond) {
t0 = props.value;
break bb0;
}
}
t0 = undefined;
}
const x = t0;
return x;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -2,7 +2,6 @@ function Component(props) {
const x = useMemo(() => {
if (props.cond) {
if (props.cond) {
return props.value;
}
}
}, [props.cond]);

View File

@@ -0,0 +1,22 @@
## Input
```javascript
function component(a) {
let x = useMemo(() => {
mutate(a);
}, []);
return x;
}
```
## Code
```javascript
function component(a) {
mutate(a);
}
```

View File

@@ -120,15 +120,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
return <Child x={state} />;
}`,
errors: [
makeTestCaseError('useMemo() callbacks must return a value'),
makeTestCaseError(
'Calling setState from useMemo may trigger an infinite loop',
),
makeTestCaseError(
'Calling setState from useMemo may trigger an infinite loop',
),
],
errors: [makeTestCaseError('Unused useMemo()')],
},
{
name: 'Pipeline errors are reported',

View File

@@ -1555,17 +1555,6 @@ const allTests = {
`,
errors: [useEffectEventError('onClick', false)],
},
{
code: normalizeIndent`
// Invalid because useEffectEvent is being passed down
function MyComponent({ theme }) {
return <Child onClick={useEffectEvent(() => {
showNotification(theme);
})} />;
}
`,
errors: [{...useEffectEventError(null, false), line: 4}],
},
{
code: normalizeIndent`
// This should error even though it shares an identifier name with the below
@@ -1737,14 +1726,6 @@ function classError(hook) {
}
function useEffectEventError(fn, called) {
if (fn === null) {
return {
message:
`React Hook "useEffectEvent" can only be called at the top level of your component.` +
` It cannot be passed down.`,
};
}
return {
message:
`\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +

View File

@@ -171,15 +171,7 @@ function isUseEffectEventIdentifier(node: Node): boolean {
return node.type === 'Identifier' && node.name === 'useEffectEvent';
}
function useEffectEventError(fn: string | null, called: boolean): string {
// no function identifier, i.e. it is not assigned to a variable
if (fn === null) {
return (
`React Hook "useEffectEvent" can only be called at the top level of your component.` +
` It cannot be passed down.`
);
}
function useEffectEventError(fn: string, called: boolean): string {
return (
`\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
'Effects and Effect Events in the same component.' +
@@ -780,22 +772,6 @@ const rule = {
// comparison later when we exit
lastEffect = node;
}
// Specifically disallow <Child onClick={useEffectEvent(...)} /> because this
// case can't be caught by `recordAllUseEffectEventFunctions` as it isn't assigned to a variable
if (
isUseEffectEventIdentifier(nodeWithoutNamespace) &&
node.parent?.type !== 'VariableDeclarator' &&
// like in other hooks, calling useEffectEvent at component's top level without assignment is valid
node.parent?.type !== 'ExpressionStatement'
) {
const message = useEffectEventError(null, false);
context.report({
node,
message,
});
}
},
Identifier(node) {

View File

@@ -136,6 +136,8 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-timeline-text-dim-color': '#ccc',
'--color-timeline-react-work-border': '#eeeeee',
'--color-timebar-background': '#f6f6f6',
'--color-timespan-background': '#62bc6a',
'--color-timespan-background-errored': '#d57066',
'--color-search-match': 'yellow',
'--color-search-match-current': '#f7923b',
'--color-selected-tree-highlight-active': 'rgba(0, 136, 250, 0.1)',
@@ -154,14 +156,6 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-warning-text-color': '#ffffff',
'--color-warning-text-color-inverted': '#fd4d69',
'--color-suspense-default': '#0088fa',
'--color-transition-default': '#6a51b2',
'--color-suspense-server': '#62bc6a',
'--color-transition-server': '#3f7844',
'--color-suspense-other': '#f3ce49',
'--color-transition-other': '#917b2c',
'--color-suspense-errored': '#d57066',
// The styles below should be kept in sync with 'root.css'
// They are repeated there because they're used by e.g. tooltips or context menus
// which get rendered outside of the DOM subtree (where normal theme/styles are written).
@@ -296,6 +290,8 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-timeline-text-dim-color': '#555b66',
'--color-timeline-react-work-border': '#3d424a',
'--color-timebar-background': '#1d2129',
'--color-timespan-background': '#62bc6a',
'--color-timespan-background-errored': '#d57066',
'--color-search-match': 'yellow',
'--color-search-match-current': '#f7923b',
'--color-selected-tree-highlight-active': 'rgba(23, 143, 185, 0.15)',
@@ -315,14 +311,6 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-warning-text-color': '#ffffff',
'--color-warning-text-color-inverted': '#ee1638',
'--color-suspense-default': '#61dafb',
'--color-transition-default': '#6a51b2',
'--color-suspense-server': '#62bc6a',
'--color-transition-server': '#3f7844',
'--color-suspense-other': '#f3ce49',
'--color-transition-other': '#917b2c',
'--color-suspense-errored': '#d57066',
// The styles below should be kept in sync with 'root.css'
// They are repeated there because they're used by e.g. tooltips or context menus
// which get rendered outside of the DOM subtree (where normal theme/styles are written).

View File

@@ -34,7 +34,6 @@ import {
shallowDiffers,
utfDecodeStringWithRanges,
parseElementDisplayNameFromBackend,
unionOfTwoArrays,
} from '../utils';
import {localStorageGetItem, localStorageSetItem} from '../storage';
import {__DEBUG__} from '../constants';
@@ -52,7 +51,6 @@ import type {
ComponentFilter,
ElementType,
SuspenseNode,
SuspenseTimelineStep,
Rect,
} from 'react-devtools-shared/src/frontend/types';
import type {
@@ -897,10 +895,13 @@ export default class Store extends EventEmitter<{
*/
getSuspendableDocumentOrderSuspense(
uniqueSuspendersOnly: boolean,
): $ReadOnlyArray<SuspenseTimelineStep> {
const target: Array<SuspenseTimelineStep> = [];
): $ReadOnlyArray<SuspenseNode['id']> {
const roots = this.roots;
let rootStep: null | SuspenseTimelineStep = null;
if (roots.length === 0) {
return [];
}
const list: SuspenseNode['id'][] = [];
for (let i = 0; i < roots.length; i++) {
const rootID = roots[i];
const root = this.getElementByID(rootID);
@@ -911,76 +912,44 @@ export default class Store extends EventEmitter<{
const suspense = this.getSuspenseByID(rootID);
if (suspense !== null) {
const environments = suspense.environments;
const environmentName =
environments.length > 0
? environments[environments.length - 1]
: null;
if (rootStep === null) {
// Arbitrarily use the first root as the root step id.
rootStep = {
id: suspense.id,
environment: environmentName,
};
target.push(rootStep);
} else if (rootStep.environment === null) {
// If any root has an environment name, then let's use it.
rootStep.environment = environmentName;
if (list.length === 0) {
// start with an arbitrary root that will allow inspection of the Screen
list.push(suspense.id);
}
const stack = [suspense];
while (stack.length > 0) {
const current = stack.pop();
if (current === undefined) {
continue;
}
// Ignore any suspense boundaries that has no visual representation as this is not
// part of the visible loading sequence.
// TODO: Consider making visible meta data and other side-effects get virtual rects.
const hasRects =
current.rects !== null &&
current.rects.length > 0 &&
current.rects.some(isNonZeroRect);
if (
hasRects &&
(!uniqueSuspendersOnly || current.hasUniqueSuspenders) &&
// Roots are already included as part of the Screen
current.id !== rootID
) {
list.push(current.id);
}
// Add children in reverse order to maintain document order
for (let j = current.children.length - 1; j >= 0; j--) {
const childSuspense = this.getSuspenseByID(current.children[j]);
if (childSuspense !== null) {
stack.push(childSuspense);
}
}
}
this.pushTimelineStepsInDocumentOrder(
suspense.children,
target,
uniqueSuspendersOnly,
environments,
);
}
}
return target;
}
pushTimelineStepsInDocumentOrder(
children: Array<SuspenseNode['id']>,
target: Array<SuspenseTimelineStep>,
uniqueSuspendersOnly: boolean,
parentEnvironments: Array<string>,
): void {
for (let i = 0; i < children.length; i++) {
const child = this.getSuspenseByID(children[i]);
if (child === null) {
continue;
}
// Ignore any suspense boundaries that has no visual representation as this is not
// part of the visible loading sequence.
// TODO: Consider making visible meta data and other side-effects get virtual rects.
const hasRects =
child.rects !== null &&
child.rects.length > 0 &&
child.rects.some(isNonZeroRect);
const childEnvironments = child.environments;
// Since children are blocked on the parent, they're also blocked by the parent environments.
// Only if we discover a novel environment do we add that and it becomes the name we use.
const unionEnvironments = unionOfTwoArrays(
parentEnvironments,
childEnvironments,
);
const environmentName =
unionEnvironments.length > 0
? unionEnvironments[unionEnvironments.length - 1]
: null;
if (hasRects && (!uniqueSuspendersOnly || child.hasUniqueSuspenders)) {
target.push({
id: child.id,
environment: environmentName,
});
}
this.pushTimelineStepsInDocumentOrder(
child.children,
target,
uniqueSuspendersOnly,
unionEnvironments,
);
}
return list;
}
getRendererIDForElement(id: number): number | null {
@@ -1658,7 +1627,6 @@ export default class Store extends EventEmitter<{
rects,
hasUniqueSuspenders: false,
isSuspended: isSuspended,
environments: [],
});
hasSuspenseTreeChanged = true;
@@ -1844,10 +1812,7 @@ export default class Store extends EventEmitter<{
envIndex++
) {
const environmentNameStringID = operations[i++];
const environmentName = stringTable[environmentNameStringID];
if (environmentName != null) {
environmentNames.push(environmentName);
}
environmentNames.push(stringTable[environmentNameStringID]);
}
const suspense = this._idToSuspense.get(id);
@@ -1871,7 +1836,7 @@ export default class Store extends EventEmitter<{
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
suspense.isSuspended = isSuspended;
suspense.environments = environmentNames;
// TODO: Recompute the environment names.
}
hasSuspenseTreeChanged = true;

View File

@@ -128,13 +128,13 @@
.TimeBarSpan, .TimeBarSpanErrored {
position: absolute;
border-radius: 0.125rem;
background-color: var(--color-suspense);
background-color: var(--color-timespan-background);
width: 100%;
height: 100%;
}
.TimeBarSpanErrored {
background-color: var(--color-suspense-errored);
background-color: var(--color-timespan-background-errored);
}
.SmallHeader {

View File

@@ -22,8 +22,6 @@ import OwnerView from './OwnerView';
import {meta} from '../../../hydration';
import useInferredName from '../useInferredName';
import {getClassNameForEnvironment} from '../SuspenseTab/SuspenseEnvironmentColors.js';
import type {
InspectedElement,
SerializedAsyncInfo,
@@ -171,7 +169,7 @@ function SuspendedByRow({
type={isOpen ? 'expanded' : 'collapsed'}
/>
<span className={styles.CollapsableHeaderTitle}>
{skipName && shortDescription !== '' ? shortDescription : name}
{skipName ? shortDescription : name}
</span>
{skipName || shortDescription === '' ? null : (
<>
@@ -183,12 +181,7 @@ function SuspendedByRow({
</>
)}
<div className={styles.CollapsableHeaderFiller} />
<div
className={
styles.TimeBarContainer +
' ' +
getClassNameForEnvironment(ioInfo.env)
}>
<div className={styles.TimeBarContainer}>
<div
className={
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
@@ -348,7 +341,6 @@ type GroupProps = {
inspectedElement: InspectedElement,
store: Store,
name: string,
environment: null | string,
suspendedBy: Array<{
index: number,
value: SerializedAsyncInfo,
@@ -363,7 +355,6 @@ function SuspendedByGroup({
inspectedElement,
store,
name,
environment,
suspendedBy,
minTime,
maxTime,
@@ -416,12 +407,7 @@ function SuspendedByGroup({
<span className={styles.CollapsableHeaderTitle}>{pluralizedName}</span>
<div className={styles.CollapsableHeaderFiller} />
{isOpen ? null : (
<div
className={
styles.TimeBarContainer +
' ' +
getClassNameForEnvironment(environment)
}>
<div className={styles.TimeBarContainer}>
<div
className={
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
@@ -516,21 +502,17 @@ export default function InspectedElementSuspendedBy({
const groups = [];
let currentGroup = null;
let currentGroupName = null;
let currentGroupEnv = null;
for (let i = 0; i < sortedSuspendedBy.length; i++) {
const entry = sortedSuspendedBy[i];
const name = entry.value.awaited.name;
const env = entry.value.awaited.env;
if (
currentGroupName !== name ||
currentGroupEnv !== env ||
!name ||
name === 'Promise' ||
currentGroup === null
) {
// Create a new group.
currentGroupName = name;
currentGroupEnv = env;
currentGroup = [];
groups.push(currentGroup);
}
@@ -609,7 +591,6 @@ export default function InspectedElementSuspendedBy({
<SuspendedByGroup
key={entries[0].index}
name={entries[0].value.awaited.name}
environment={entries[0].value.awaited.env}
suspendedBy={entries}
bridge={bridge}
element={element}

View File

@@ -1,14 +0,0 @@
.SuspenseEnvironmentDefault {
--color-suspense: var(--color-suspense-default);
--color-transition: var(--color-transition-default);
}
.SuspenseEnvironmentServer {
--color-suspense: var(--color-suspense-server);
--color-transition: var(--color-transition-server);
}
.SuspenseEnvironmentOther {
--color-suspense: var(--color-suspense-other);
--color-transition: var(--color-transition-other);
}

View File

@@ -1,20 +0,0 @@
/**
* 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.
*
* @flow
*/
import styles from './SuspenseEnvironmentColors.css';
export function getClassNameForEnvironment(environment: null | string): string {
if (environment === null) {
return styles.SuspenseEnvironmentDefault;
}
if (environment === 'Server') {
return styles.SuspenseEnvironmentServer;
}
return styles.SuspenseEnvironmentOther;
}

View File

@@ -1,25 +1,12 @@
.SuspenseRectsContainer {
padding: .25rem;
outline-color: transparent;
outline-style: solid;
outline-width: 1px;
cursor: pointer;
outline: 1px solid var(--color-component-name);
border-radius: 0.25rem;
}
.SuspenseRectsContainer[data-highlighted='true'] {
outline-color: var(--color-transition);
outline-style: solid;
outline-width: 4px;
}
.SuspenseRectsRoot {
cursor: pointer;
outline-color: var(--color-transition);
background-color: color-mix(in srgb, var(--color-transition) 5%, transparent);
}
.SuspenseRectsRoot[data-hovered='true'] {
background-color: color-mix(in srgb, var(--color-transition) 15%, transparent);
background: var(--color-dimmest);
}
.SuspenseRectsViewBox {
@@ -28,11 +15,6 @@
.SuspenseRectsBoundary {
pointer-events: all;
border-radius: 0.125rem;
}
.SuspenseRectsBoundary[data-visible='false'] {
background-color: transparent;
}
.SuspenseRectsBoundaryChildren {
@@ -46,18 +28,15 @@
.SuspenseRectsRect {
box-shadow: var(--elevation-4);
pointer-events: all;
cursor: pointer;
border-radius: 0.125rem;
background-color: color-mix(in srgb, var(--color-background) 50%, var(--color-suspense) 25%);
backdrop-filter: grayscale(100%);
transition: background-color 0.2s ease-in;
outline-color: var(--color-suspense);
outline-style: solid;
outline-width: 1px;
border-radius: 0.125rem;
cursor: pointer;
}
.SuspenseRectsScaledRect {
position: absolute;
outline-color: var(--color-background-selected);
}
.SuspenseRectsScaledRect[data-visible='false'] {
@@ -65,28 +44,15 @@
outline-width: 0;
}
.SuspenseRectsBoundary[data-suspended='true'] {
opacity: 0.33;
.SuspenseRectsScaledRect[data-suspended='true'] {
opacity: 0.3;
}
/* highlight this boundary */
.SuspenseRectsBoundary[data-hovered='true'] > .SuspenseRectsRect {
background-color: color-mix(in srgb, var(--color-background) 50%, var(--color-suspense) 50%);
transition: background-color 0.2s ease-out;
.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
background-color: var(--color-background-hover);
}
.SuspenseRectsBoundary[data-selected='true'] {
box-shadow: var(--elevation-4);
}
.SuspenseRectOutline {
outline-color: var(--color-suspense);
outline-style: solid;
outline-width: 4px;
border-radius: 0.125rem;
pointer-events: none;
}
.SuspenseRectsBoundary[data-selected='true'] > .SuspenseRectsRect {
box-shadow: none;
.SuspenseRectsRect[data-highlighted='true'] {
background-color: var(--color-selected-tree-highlight-active);
}

View File

@@ -30,15 +30,12 @@ import {
SuspenseTreeStateContext,
SuspenseTreeDispatcherContext,
} from './SuspenseTreeContext';
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
function ScaledRect({
className,
rect,
visible,
suspended,
selected,
hovered,
adjust,
...props
}: {
@@ -46,8 +43,6 @@ function ScaledRect({
rect: Rect,
visible: boolean,
suspended: boolean,
selected?: boolean,
hovered?: boolean,
adjust?: boolean,
...
}): React$Node {
@@ -63,8 +58,6 @@ function ScaledRect({
className={styles.SuspenseRectsScaledRect + ' ' + className}
data-visible={visible}
data-suspended={suspended}
data-selected={selected}
data-hovered={hovered}
style={{
// Shrink one pixel so that the bottom outline will line up with the top outline of the next one.
width: adjust ? 'calc(' + width + ' - 1px)' : width,
@@ -84,9 +77,7 @@ function SuspenseRects({
const store = useContext(StoreContext);
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
const {uniqueSuspendersOnly, timeline, hoveredTimelineIndex} = useContext(
SuspenseTreeStateContext,
);
const {uniqueSuspendersOnly} = useContext(SuspenseTreeStateContext);
const {inspectedElementID} = useContext(TreeStateContext);
@@ -154,33 +145,14 @@ function SuspenseRects({
// TODO: Use the nearest Suspense boundary
const selected = inspectedElementID === suspenseID;
const hovered =
hoveredTimelineIndex > -1 &&
timeline[hoveredTimelineIndex].id === suspenseID;
let environment: null | string = null;
for (let i = 0; i < timeline.length; i++) {
const timelineStep = timeline[i];
if (timelineStep.id === suspenseID) {
environment = timelineStep.environment;
break;
}
}
const boundingBox = getBoundingBox(suspense.rects);
return (
<ScaledRect
rect={boundingBox}
className={
styles.SuspenseRectsBoundary +
' ' +
getClassNameForEnvironment(environment)
}
className={styles.SuspenseRectsBoundary}
visible={visible}
selected={selected}
suspended={suspense.isSuspended}
hovered={hovered}>
suspended={suspense.isSuspended}>
<ViewBox.Provider value={boundingBox}>
{visible &&
suspense.rects !== null &&
@@ -190,6 +162,7 @@ function SuspenseRects({
key={index}
className={styles.SuspenseRectsRect}
rect={rect}
data-highlighted={selected}
adjust={true}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
@@ -209,13 +182,6 @@ function SuspenseRects({
})}
</ScaledRect>
)}
{selected ? (
<ScaledRect
className={styles.SuspenseRectOutline}
rect={boundingBox}
adjust={true}
/>
) : null}
</ViewBox.Provider>
</ScaledRect>
);
@@ -341,8 +307,7 @@ function SuspenseRectsContainer(): React$Node {
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
const {roots, timeline, hoveredTimelineIndex, uniqueSuspendersOnly} =
useContext(SuspenseTreeStateContext);
const {roots} = useContext(SuspenseTreeStateContext);
// TODO: bbox does not consider uniqueSuspendersOnly filter
const boundingBox = getDocumentBoundingRect(store, roots);
@@ -386,37 +351,13 @@ function SuspenseRectsContainer(): React$Node {
}
const isRootSelected = roots.includes(inspectedElementID);
const isRootHovered = hoveredTimelineIndex === 0;
let hasRootSuspenders = false;
if (!uniqueSuspendersOnly) {
hasRootSuspenders = true;
} else {
for (let i = 0; i < roots.length; i++) {
const rootID = roots[i];
const root = store.getSuspenseByID(rootID);
if (root !== null && root.hasUniqueSuspenders) {
hasRootSuspenders = true;
break;
}
}
}
const rootEnvironment =
timeline.length === 0 ? null : timeline[0].environment;
return (
<div
className={
styles.SuspenseRectsContainer +
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '') +
' ' +
getClassNameForEnvironment(rootEnvironment)
}
className={styles.SuspenseRectsContainer}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
data-highlighted={isRootSelected}
data-hovered={isRootHovered}>
data-highlighted={isRootSelected}>
<ViewBox.Provider value={boundingBox}>
<div
className={styles.SuspenseRectsViewBox}

View File

@@ -40,21 +40,22 @@
.SuspenseScrubberBead {
flex: 1;
height: 0.5rem;
background: var(--color-background-selected);
border-radius: 0.5rem;
background: color-mix(in srgb, var(--color-suspense) 10%, transparent);
transition: all 0.3s ease-in;
background: var(--color-selected-tree-highlight-active);
transition: all 0.3s ease-in-out;
}
.SuspenseScrubberBeadSelected {
height: 1rem;
background: var(--color-suspense);
background: var(--color-background-selected);
}
.SuspenseScrubberBeadTransition {
background: var(--color-transition);
background: var(--color-component-name);
}
.SuspenseScrubberStepHighlight > .SuspenseScrubberBead {
.SuspenseScrubberStepHighlight > .SuspenseScrubberBead,
.SuspenseScrubberStep:hover > .SuspenseScrubberBead {
height: 0.75rem;
transition: all 0.3s ease-out;
}

View File

@@ -7,8 +7,6 @@
* @flow
*/
import type {SuspenseTimelineStep} from 'react-devtools-shared/src/frontend/types';
import typeof {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
import * as React from 'react';
@@ -16,14 +14,11 @@ import {useRef} from 'react';
import styles from './SuspenseScrubber.css';
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
import Tooltip from '../Components/reach-ui/tooltip';
export default function SuspenseScrubber({
min,
max,
timeline,
value,
highlight,
onBlur,
@@ -34,7 +29,6 @@ export default function SuspenseScrubber({
}: {
min: number,
max: number,
timeline: $ReadOnlyArray<SuspenseTimelineStep>,
value: number,
highlight: number,
onBlur?: () => void,
@@ -60,18 +54,17 @@ export default function SuspenseScrubber({
}
const steps = [];
for (let index = min; index <= max; index++) {
const environment = timeline[index].environment;
const label =
index === min
? // The first step in the timeline is always a Transition (Initial Paint).
'Initial Paint' +
(environment === null ? '' : ' (' + environment + ')')
: // TODO: Consider adding the name of this specific boundary if this step has only one.
environment === null
? 'Suspense'
: environment;
steps.push(
<Tooltip key={index} label={label}>
<Tooltip
key={index}
label={
index === min
? // The first step in the timeline is always a Transition (Initial Paint).
// TODO: Support multiple environments.
'Initial Paint'
: // TODO: Consider adding the name of this specific boundary if this step has only one.
'Suspense'
}>
<div
className={
styles.SuspenseScrubberStep +
@@ -86,10 +79,9 @@ export default function SuspenseScrubber({
styles.SuspenseScrubberBead +
(index === min
? // The first step in the timeline is always a Transition (Initial Paint).
// TODO: Support multiple environments.
' ' + styles.SuspenseScrubberBeadTransition
: '') +
' ' +
getClassNameForEnvironment(environment) +
(index <= value ? ' ' + styles.SuspenseScrubberBeadSelected : '')
}
/>

View File

@@ -34,7 +34,7 @@ function SuspenseTimelineInput() {
const max = timeline.length > 0 ? timeline.length - 1 : 0;
function switchSuspenseNode(nextTimelineIndex: number) {
const nextSelectedSuspenseID = timeline[nextTimelineIndex].id;
const nextSelectedSuspenseID = timeline[nextTimelineIndex];
treeDispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: nextSelectedSuspenseID,
@@ -53,22 +53,13 @@ function SuspenseTimelineInput() {
switchSuspenseNode(timelineIndex);
}
function handleHoverSegment(hoveredIndex: number) {
const nextSelectedSuspenseID = timeline[hoveredIndex].id;
suspenseTreeDispatch({
type: 'HOVER_TIMELINE_FOR_ID',
payload: nextSelectedSuspenseID,
});
}
function handleUnhoverSegment() {
suspenseTreeDispatch({
type: 'HOVER_TIMELINE_FOR_ID',
payload: -1,
});
function handleHoverSegment(hoveredValue: number) {
// TODO: Consider highlighting the rect instead.
}
function handleUnhoverSegment() {}
function skipPrevious() {
const nextSelectedSuspenseID = timeline[timelineIndex - 1].id;
const nextSelectedSuspenseID = timeline[timelineIndex - 1];
treeDispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: nextSelectedSuspenseID,
@@ -80,7 +71,7 @@ function SuspenseTimelineInput() {
}
function skipForward() {
const nextSelectedSuspenseID = timeline[timelineIndex + 1].id;
const nextSelectedSuspenseID = timeline[timelineIndex + 1];
treeDispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: nextSelectedSuspenseID,
@@ -106,7 +97,7 @@ function SuspenseTimelineInput() {
// anything suspended in the root. The step after that should have one less
// thing suspended. I.e. the first suspense boundary should be unsuspended
// when it's selected. This also lets you show everything in the last step.
const suspendedSet = timeline.slice(timelineIndex + 1).map(step => step.id);
const suspendedSet = timeline.slice(timelineIndex + 1);
bridge.send('overrideSuspenseMilestone', {
suspendedSet,
});
@@ -173,7 +164,6 @@ function SuspenseTimelineInput() {
<SuspenseScrubber
min={min}
max={max}
timeline={timeline}
value={timelineIndex}
highlight={hoveredTimelineIndex}
onChange={handleChange}

View File

@@ -7,10 +7,7 @@
* @flow
*/
import type {ReactContext} from 'shared/ReactTypes';
import type {
SuspenseNode,
SuspenseTimelineStep,
} from 'react-devtools-shared/src/frontend/types';
import type {SuspenseNode} from 'react-devtools-shared/src/frontend/types';
import type Store from '../../store';
import * as React from 'react';
@@ -28,7 +25,7 @@ export type SuspenseTreeState = {
lineage: $ReadOnlyArray<SuspenseNode['id']> | null,
roots: $ReadOnlyArray<SuspenseNode['id']>,
selectedSuspenseID: SuspenseNode['id'] | null,
timeline: $ReadOnlyArray<SuspenseTimelineStep>,
timeline: $ReadOnlyArray<SuspenseNode['id']>,
timelineIndex: number | -1,
hoveredTimelineIndex: number | -1,
uniqueSuspendersOnly: boolean,
@@ -52,7 +49,7 @@ type ACTION_SELECT_SUSPENSE_BY_ID = {
type ACTION_SET_SUSPENSE_TIMELINE = {
type: 'SET_SUSPENSE_TIMELINE',
payload: [
$ReadOnlyArray<SuspenseTimelineStep>,
$ReadOnlyArray<SuspenseNode['id']>,
// The next Suspense ID to select in the timeline
SuspenseNode['id'] | null,
// Whether this timeline includes only unique suspenders
@@ -114,7 +111,7 @@ function getInitialState(store: Store): SuspenseTreeState {
store.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
const timelineIndex = timeline.length - 1;
const selectedSuspenseID =
timelineIndex === -1 ? null : timeline[timelineIndex].id;
timelineIndex === -1 ? null : timeline[timelineIndex];
const lineage =
selectedSuspenseID !== null
? store.getSuspenseLineage(selectedSuspenseID)
@@ -167,18 +164,16 @@ function SuspenseTreeContextController({children}: Props): React.Node {
selectedSuspenseID = null;
}
const selectedTimelineStep =
state.timeline === null || state.timelineIndex === -1
let selectedTimelineID =
state.timeline === null
? null
: state.timeline[state.timelineIndex];
let selectedTimelineID: null | number = null;
if (selectedTimelineStep !== null) {
selectedTimelineID = selectedTimelineStep.id;
// $FlowFixMe
while (removedIDs.has(selectedTimelineID)) {
// $FlowFixMe
selectedTimelineID = removedIDs.get(selectedTimelineID);
}
while (
selectedTimelineID !== null &&
removedIDs.has(selectedTimelineID)
) {
// $FlowExpectedError[incompatible-type]
selectedTimelineID = removedIDs.get(selectedTimelineID);
}
// TODO: Handle different timeline modes (e.g. random order)
@@ -186,25 +181,20 @@ function SuspenseTreeContextController({children}: Props): React.Node {
state.uniqueSuspendersOnly,
);
let nextTimelineIndex = -1;
if (selectedTimelineID !== null && nextTimeline.length !== 0) {
for (let i = 0; i < nextTimeline.length; i++) {
if (nextTimeline[i].id === selectedTimelineID) {
nextTimelineIndex = i;
break;
}
}
}
let nextTimelineIndex =
selectedTimelineID === null || nextTimeline.length === 0
? -1
: nextTimeline.indexOf(selectedTimelineID);
if (
nextTimeline.length > 0 &&
(nextTimelineIndex === -1 || state.autoSelect)
) {
nextTimelineIndex = nextTimeline.length - 1;
selectedSuspenseID = nextTimeline[nextTimelineIndex].id;
selectedSuspenseID = nextTimeline[nextTimelineIndex];
}
if (selectedSuspenseID === null && nextTimeline.length > 0) {
selectedSuspenseID = nextTimeline[nextTimeline.length - 1].id;
selectedSuspenseID = nextTimeline[nextTimeline.length - 1];
}
const nextLineage =
@@ -266,12 +256,12 @@ function SuspenseTreeContextController({children}: Props): React.Node {
nextMilestoneIndex = nextTimeline.indexOf(previousMilestoneID);
if (nextMilestoneIndex === -1 && nextTimeline.length > 0) {
nextMilestoneIndex = nextTimeline.length - 1;
nextSelectedSuspenseID = nextTimeline[nextMilestoneIndex].id;
nextSelectedSuspenseID = nextTimeline[nextMilestoneIndex];
nextLineage = store.getSuspenseLineage(nextSelectedSuspenseID);
}
} else if (nextRootID !== null) {
nextMilestoneIndex = nextTimeline.length - 1;
nextSelectedSuspenseID = nextTimeline[nextMilestoneIndex].id;
nextSelectedSuspenseID = nextTimeline[nextMilestoneIndex];
nextLineage = store.getSuspenseLineage(nextSelectedSuspenseID);
}
@@ -286,7 +276,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
}
case 'SUSPENSE_SET_TIMELINE_INDEX': {
const nextTimelineIndex = action.payload;
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
const nextLineage = store.getSuspenseLineage(
nextSelectedSuspenseID,
);
@@ -311,7 +301,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
) {
return state;
}
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
const nextLineage = store.getSuspenseLineage(
nextSelectedSuspenseID,
);
@@ -339,7 +329,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
) {
// If we're restarting at the end. Then loop around and start again from the beginning.
nextTimelineIndex = 0;
nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
nextLineage = store.getSuspenseLineage(nextSelectedSuspenseID);
}
@@ -362,7 +352,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
if (nextTimelineIndex > state.timeline.length - 1) {
return state;
}
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
const nextLineage = store.getSuspenseLineage(
nextSelectedSuspenseID,
);
@@ -379,14 +369,8 @@ function SuspenseTreeContextController({children}: Props): React.Node {
}
case 'TOGGLE_TIMELINE_FOR_ID': {
const suspenseID = action.payload;
let timelineIndexForSuspenseID = -1;
for (let i = 0; i < state.timeline.length; i++) {
if (state.timeline[i].id === suspenseID) {
timelineIndexForSuspenseID = i;
break;
}
}
const timelineIndexForSuspenseID =
state.timeline.indexOf(suspenseID);
if (timelineIndexForSuspenseID === -1) {
// This boundary is no longer in the timeline.
return state;
@@ -403,7 +387,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
timelineIndexForSuspenseID
: // Otherwise, if we're currently showing it, jump to right before to hide it.
timelineIndexForSuspenseID - 1;
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex].id;
const nextSelectedSuspenseID = state.timeline[nextTimelineIndex];
const nextLineage = store.getSuspenseLineage(
nextSelectedSuspenseID,
);
@@ -419,13 +403,8 @@ function SuspenseTreeContextController({children}: Props): React.Node {
}
case 'HOVER_TIMELINE_FOR_ID': {
const suspenseID = action.payload;
let timelineIndexForSuspenseID = -1;
for (let i = 0; i < state.timeline.length; i++) {
if (state.timeline[i].id === suspenseID) {
timelineIndexForSuspenseID = i;
break;
}
}
const timelineIndexForSuspenseID =
state.timeline.indexOf(suspenseID);
return {
...state,
hoveredTimelineIndex: timelineIndexForSuspenseID,

View File

@@ -193,11 +193,6 @@ export type Rect = {
height: number,
};
export type SuspenseTimelineStep = {
id: SuspenseNode['id'], // TODO: Will become a group.
environment: null | string,
};
export type SuspenseNode = {
id: Element['id'],
parentID: SuspenseNode['id'] | 0,
@@ -206,7 +201,6 @@ export type SuspenseNode = {
rects: null | Array<Rect>,
hasUniqueSuspenders: boolean,
isSuspended: boolean,
environments: Array<string>,
};
// Serialized version of ReactIOInfo

View File

@@ -1305,18 +1305,3 @@ export function onReloadAndProfileFlagsReset(): void {
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_TIMELINE_KEY);
}
export function unionOfTwoArrays<T>(a: Array<T>, b: Array<T>): Array<T> {
let result = a;
for (let i = 0; i < b.length; i++) {
const value = b[i];
if (a.indexOf(value) === -1) {
if (result === a) {
// Lazily copy
result = a.slice(0);
}
result.push(value);
}
}
return result;
}

View File

@@ -10,11 +10,11 @@
'use strict';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
let clientExports;
let webpackMap;
let webpackModules;
@@ -1136,37 +1136,4 @@ describe('ReactFlightDOMNode', () => {
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
);
});
// This is a regression test for a specific issue where byte Web Streams are
// detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
// perform Construct on a detached ArrayBuffer") for chunks that are using
// Node's internal Buffer pool.
it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams', async () => {
// Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
const file = path.join(os.tmpdir(), 'test.bin');
fs.writeFileSync(file, Buffer.alloc(4095));
const fileChunk = fs.readFileSync(file);
fs.unlinkSync(file);
// Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
expect(fileChunk.buffer.byteLength).toBe(8192);
const readable = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(fileChunk, webpackMap),
);
// Create a Web Streams WritableStream that tries to use Buffer operations.
const writable = new WritableStream({
write(chunk) {
// Only write one byte to ensure Node.js is not creating a new Buffer
// pool. Typically, library code (e.g. a compression middleware) would
// call Buffer.from(chunk) or similar, instead of allocating a new
// Buffer directly. With that, the test file could only be ~2600 bytes.
Buffer.allocUnsafe(1);
},
});
// Must not throw an error.
await readable.pipeTo(writable);
});
});

View File

@@ -37,11 +37,7 @@ export function flushBuffered(destination: Destination) {
// transform streams. https://github.com/whatwg/streams/issues/960
}
// Chunks larger than VIEW_SIZE are written directly, without copying into the
// internal view buffer. This must be at least half of Node's internal Buffer
// pool size (8192) to avoid corrupting the pool when using
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
const VIEW_SIZE = 4096;
const VIEW_SIZE = 2048;
let currentView = null;
let writtenBytes = 0;
@@ -151,7 +147,14 @@ export function typedArrayToBinaryChunk(
// If we passed through this straight to enqueue we wouldn't have to convert it but since
// we need to copy the buffer in that case, we need to convert it to copy it.
// When we copy it into another array using set() it needs to be a Uint8Array.
return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
const buffer = new Uint8Array(
content.buffer,
content.byteOffset,
content.byteLength,
);
// We clone large chunks so that we can transfer them when we write them.
// Others get copied into the target buffer.
return content.byteLength > VIEW_SIZE ? buffer.slice() : buffer;
}
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {

View File

@@ -38,11 +38,7 @@ export function flushBuffered(destination: Destination) {
}
}
// Chunks larger than VIEW_SIZE are written directly, without copying into the
// internal view buffer. This must be at least half of Node's internal Buffer
// pool size (8192) to avoid corrupting the pool when using
// renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
const VIEW_SIZE = 4096;
const VIEW_SIZE = 2048;
let currentView = null;
let writtenBytes = 0;
let destinationHasCapacity = true;

View File

@@ -1,9 +1,3 @@
/**
* 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';
const babel = require('@babel/register');