Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55bf051afd | ||
|
|
2a18d35301 |
@@ -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({
|
||||
/*
|
||||
|
||||
@@ -988,7 +988,7 @@ export function createTemporaryPlace(
|
||||
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
|
||||
reactive: false,
|
||||
effect: Effect.Unknown,
|
||||
loc: GeneratedSource,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
CallExpression,
|
||||
Effect,
|
||||
Environment,
|
||||
FinishMemoize,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
@@ -25,7 +24,6 @@ import {
|
||||
Place,
|
||||
PropertyLoad,
|
||||
SpreadPattern,
|
||||
StartMemoize,
|
||||
TInstruction,
|
||||
getHookKindForType,
|
||||
makeInstructionId,
|
||||
@@ -184,36 +182,52 @@ function makeManualMemoizationMarkers(
|
||||
depsList: Array<ManualMemoDependency> | null,
|
||||
memoDecl: Place,
|
||||
manualMemoId: number,
|
||||
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
|
||||
): [Array<Instruction>, Array<Instruction>] {
|
||||
const temp = createTemporaryPlace(env, memoDecl.loc);
|
||||
return [
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env, fnExpr.loc),
|
||||
value: {
|
||||
kind: 'StartMemoize',
|
||||
manualMemoId,
|
||||
/*
|
||||
* Use deps list from source instead of inferred deps
|
||||
* as dependencies
|
||||
*/
|
||||
deps: depsList,
|
||||
[
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env, fnExpr.loc),
|
||||
value: {
|
||||
kind: 'StartMemoize',
|
||||
manualMemoId,
|
||||
/*
|
||||
* Use deps list from source instead of inferred deps
|
||||
* as dependencies
|
||||
*/
|
||||
deps: depsList,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env, fnExpr.loc),
|
||||
value: {
|
||||
kind: 'FinishMemoize',
|
||||
manualMemoId,
|
||||
decl: {...memoDecl},
|
||||
loc: fnExpr.loc,
|
||||
],
|
||||
[
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...temp},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {...memoDecl},
|
||||
loc: memoDecl.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: memoDecl.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
{
|
||||
id: makeInstructionId(0),
|
||||
lvalue: createTemporaryPlace(env, memoDecl.loc),
|
||||
value: {
|
||||
kind: 'FinishMemoize',
|
||||
manualMemoId,
|
||||
decl: {...temp},
|
||||
loc: memoDecl.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: memoDecl.loc,
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -409,10 +423,7 @@ export function dropManualMemoization(
|
||||
* LoadLocal fnArg
|
||||
* - (if validation is enabled) collect manual memoization markers
|
||||
*/
|
||||
const queuedInserts: Map<
|
||||
InstructionId,
|
||||
TInstruction<StartMemoize> | TInstruction<FinishMemoize>
|
||||
> = new Map();
|
||||
const queuedInserts: Map<InstructionId, Array<Instruction>> = new Map();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (let i = 0; i < block.instructions.length; i++) {
|
||||
const instr = block.instructions[i]!;
|
||||
@@ -523,11 +534,11 @@ export function dropManualMemoization(
|
||||
let nextInstructions: Array<Instruction> | null = null;
|
||||
for (let i = 0; i < block.instructions.length; i++) {
|
||||
const instr = block.instructions[i];
|
||||
const insertInstr = queuedInserts.get(instr.id);
|
||||
if (insertInstr != null) {
|
||||
const insertInstructions = queuedInserts.get(instr.id);
|
||||
if (insertInstructions != null) {
|
||||
nextInstructions = nextInstructions ?? block.instructions.slice(0, i);
|
||||
nextInstructions.push(instr);
|
||||
nextInstructions.push(insertInstr);
|
||||
nextInstructions.push(...insertInstructions);
|
||||
} else if (nextInstructions != null) {
|
||||
nextInstructions.push(instr);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,15 @@ class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FinishMemoize': {
|
||||
if (
|
||||
!withinScope &&
|
||||
this.alwaysInvalidatingValues.has(value.decl.identifier)
|
||||
) {
|
||||
value.pruned = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {kind: 'keep'};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -184,28 +184,25 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ Found 1 error:
|
||||
|
||||
Invariant: Expected consistent kind for destructuring
|
||||
|
||||
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
|
||||
Other places were `Reassign` but 'mutate? #t8$47[7:9]{reactive}' is const.
|
||||
|
||||
error.bug-invariant-expected-consistent-destructuring.ts:9:9
|
||||
7 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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'}],
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
// useMemo values may not be memoized in Forget output if we
|
||||
// infer that their deps always invalidate.
|
||||
// This is technically a false positive as the useMemo in source
|
||||
// was effectively a no-op
|
||||
function useFoo(props) {
|
||||
const x = [];
|
||||
useHook();
|
||||
x.push(props);
|
||||
|
||||
return useMemo(() => [x], [x]);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
|
||||
|
||||
error.false-positive-useMemo-dropped-infer-always-invalidating.ts:15:9
|
||||
13 | x.push(props);
|
||||
14 |
|
||||
> 15 | return useMemo(() => [x], [x]);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization
|
||||
16 | }
|
||||
17 |
|
||||
18 | export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
// useMemo values may not be memoized in Forget output if we
|
||||
// infer that their deps always invalidate.
|
||||
// This is technically a false positive as the useMemo in source
|
||||
// was effectively a no-op
|
||||
function useFoo(props) {
|
||||
const x = [];
|
||||
useHook();
|
||||
x.push(props);
|
||||
|
||||
return useMemo(() => [x], [x]);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
// If we can prove that a useMemo was ineffective because it would always invalidate,
|
||||
// then we shouldn't throw a "couldn't preserve existing memoization" error
|
||||
// TODO: consider reporting a separate error to the user for this case, if you're going
|
||||
// to memoize manually, then you probably want to know that it's a no-op
|
||||
function useFoo(props) {
|
||||
const x = [];
|
||||
useHook();
|
||||
x.push(props);
|
||||
|
||||
return useMemo(() => [x], [x]);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useHook } from "shared-runtime";
|
||||
|
||||
// If we can prove that a useMemo was ineffective because it would always invalidate,
|
||||
// then we shouldn't throw a "couldn't preserve existing memoization" error
|
||||
// TODO: consider reporting a separate error to the user for this case, if you're going
|
||||
// to memoize manually, then you probably want to know that it's a no-op
|
||||
function useFoo(props) {
|
||||
const x = [];
|
||||
useHook();
|
||||
x.push(props);
|
||||
return [x];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [[{}]]
|
||||
@@ -0,0 +1,21 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
// If we can prove that a useMemo was ineffective because it would always invalidate,
|
||||
// then we shouldn't throw a "couldn't preserve existing memoization" error
|
||||
// TODO: consider reporting a separate error to the user for this case, if you're going
|
||||
// to memoize manually, then you probably want to know that it's a no-op
|
||||
function useFoo(props) {
|
||||
const x = [];
|
||||
useHook();
|
||||
x.push(props);
|
||||
|
||||
return useMemo(() => [x], [x]);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -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
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {identity, useIdentity} from 'shared-runtime';
|
||||
|
||||
// Adapted from https://github.com/facebook/react/issues/34750
|
||||
function useLocalCampaignBySlug(slug: string) {
|
||||
const campaigns = useIdentity({a: {slug: 'a', name: 'campaign'}});
|
||||
// The useMemo result is never assigned to a local so we did not previously ensure
|
||||
// that there was a variable declaration for it when promoting the result temporary
|
||||
return useMemo(() => {
|
||||
for (const id of Object.keys(campaigns)) {
|
||||
const campaign = campaigns[id];
|
||||
if (campaign.slug === slug) {
|
||||
return identity(campaign);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [campaigns, slug]);
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const campaign = useLocalCampaignBySlug('a');
|
||||
return <div>{campaign.name}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import { identity, useIdentity } from "shared-runtime";
|
||||
|
||||
// Adapted from https://github.com/facebook/react/issues/34750
|
||||
function useLocalCampaignBySlug(slug) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = { a: { slug: "a", name: "campaign" } };
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const campaigns = useIdentity(t0);
|
||||
let t1;
|
||||
if ($[1] !== campaigns || $[2] !== slug) {
|
||||
bb0: {
|
||||
for (const id of Object.keys(campaigns)) {
|
||||
const campaign = campaigns[id];
|
||||
if (campaign.slug === slug) {
|
||||
t1 = identity(campaign);
|
||||
break bb0;
|
||||
}
|
||||
}
|
||||
|
||||
t1 = null;
|
||||
}
|
||||
$[1] = campaigns;
|
||||
$[2] = slug;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const campaign = useLocalCampaignBySlug("a");
|
||||
let t0;
|
||||
if ($[0] !== campaign.name) {
|
||||
t0 = <div>{campaign.name}</div>;
|
||||
$[0] = campaign.name;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>campaign</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
import {useMemo} from 'react';
|
||||
import {identity, useIdentity} from 'shared-runtime';
|
||||
|
||||
// Adapted from https://github.com/facebook/react/issues/34750
|
||||
function useLocalCampaignBySlug(slug: string) {
|
||||
const campaigns = useIdentity({a: {slug: 'a', name: 'campaign'}});
|
||||
// The useMemo result is never assigned to a local so we did not previously ensure
|
||||
// that there was a variable declaration for it when promoting the result temporary
|
||||
return useMemo(() => {
|
||||
for (const id of Object.keys(campaigns)) {
|
||||
const campaign = campaigns[id];
|
||||
if (campaign.slug === slug) {
|
||||
return identity(campaign);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [campaigns, slug]);
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const campaign = useLocalCampaignBySlug('a');
|
||||
return <div>{campaign.name}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -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).
|
||||
|
||||
119
packages/react-devtools-shared/src/devtools/store.js
vendored
119
packages/react-devtools-shared/src/devtools/store.js
vendored
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 : '')
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
15
packages/react-devtools-shared/src/utils.js
vendored
15
packages/react-devtools-shared/src/utils.js
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user