Compare commits
38 Commits
pr33573
...
mcp-sync-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
555f844195 | ||
|
|
c94e8b4461 | ||
|
|
6b04874535 | ||
|
|
1ba1485a65 | ||
|
|
2c1e4e4513 | ||
|
|
e67b4fe22e | ||
|
|
4a523489b7 | ||
|
|
94cf60bede | ||
|
|
bbc13fa17b | ||
|
|
12eaef7ef5 | ||
|
|
c80c69fa96 | ||
|
|
aab72cb1cb | ||
|
|
fa3feba672 | ||
|
|
2a911f27dd | ||
|
|
18ee505e77 | ||
|
|
1d1b26c701 | ||
|
|
fe3f0ec037 | ||
|
|
d70ee32b88 | ||
|
|
6c7b1a1d98 | ||
|
|
ed077194b5 | ||
|
|
643257ca52 | ||
|
|
06e89951be | ||
|
|
79d9aed7ed | ||
|
|
c8822e926b | ||
|
|
a947eba4f2 | ||
|
|
374dfe8edf | ||
|
|
2bee34867d | ||
|
|
d37faa041b | ||
|
|
3a2ff8b51b | ||
|
|
cc3806377a | ||
|
|
4f543f326c | ||
|
|
7ceb10035f | ||
|
|
4335f69987 | ||
|
|
34179fe344 | ||
|
|
0e7cdebb32 | ||
|
|
81d8115116 | ||
|
|
8f4ce72f0b | ||
|
|
7ce2a63acc |
10
.github/workflows/runtime_build_and_test.yml
vendored
10
.github/workflows/runtime_build_and_test.yml
vendored
@@ -301,10 +301,12 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Install runtime dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Install compiler dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: compiler
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
|
||||
- run: yarn workspace eslint-plugin-react-hooks test
|
||||
|
||||
8
.github/workflows/runtime_prereleases.yml
vendored
8
.github/workflows/runtime_prereleases.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ inputs.skip_packages }}'
|
||||
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
|
||||
run: |
|
||||
@@ -94,19 +94,19 @@ jobs:
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- name: Notify Discord on failure
|
||||
if: failure() && inputs.enableFailureNotification == true
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: "GitHub Actions"
|
||||
embed-title: 'Publish of $${{ inputs.release_channel }} release failed'
|
||||
embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
|
||||
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
--tags=${{ inputs.tags }} \
|
||||
--publishVersion=${{ inputs.version_to_publish }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ inputs.skip_packages }}'
|
||||
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
|
||||
run: |
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
--tags=${{ inputs.tags }} \
|
||||
--publishVersion=${{ inputs.version_to_publish }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- name: Archive released package for debugging
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Effect, makeIdentifierId, ValueKind, ValueReason} from './HIR';
|
||||
import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
addFunction,
|
||||
addHook,
|
||||
addObject,
|
||||
signatureArgument,
|
||||
} from './ObjectShape';
|
||||
import {BuiltInType, ObjectType, PolyType} from './Types';
|
||||
import {TypeConfig} from './TypeSchema';
|
||||
@@ -646,35 +645,35 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
hookKind: 'useEffect',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [signatureArgument(3)],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: ['@effect'],
|
||||
effects: [
|
||||
// Freezes the function and deps
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: signatureArgument(1),
|
||||
value: '@rest',
|
||||
reason: ValueReason.Effect,
|
||||
},
|
||||
// Internally creates an effect object that captures the function and deps
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(3),
|
||||
into: '@effect',
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The effect stores the function and dependencies
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(3),
|
||||
from: '@rest',
|
||||
into: '@effect',
|
||||
},
|
||||
// Returns undefined
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
into: '@returns',
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
@@ -906,6 +905,7 @@ export function installTypeConfig(
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
mutableOnlyIfOperandsAreMutable:
|
||||
typeConfig.mutableOnlyIfOperandsAreMutable === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
});
|
||||
}
|
||||
case 'hook': {
|
||||
@@ -923,6 +923,7 @@ export function installTypeConfig(
|
||||
),
|
||||
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
});
|
||||
}
|
||||
case 'object': {
|
||||
|
||||
@@ -1453,6 +1453,20 @@ export const ValueKindSchema = z.enum([
|
||||
ValueKind.Context,
|
||||
]);
|
||||
|
||||
export const ValueReasonSchema = z.enum([
|
||||
ValueReason.Context,
|
||||
ValueReason.Effect,
|
||||
ValueReason.Global,
|
||||
ValueReason.HookCaptured,
|
||||
ValueReason.HookReturn,
|
||||
ValueReason.JsxCaptured,
|
||||
ValueReason.KnownReturnSignature,
|
||||
ValueReason.Other,
|
||||
ValueReason.ReactiveFunctionArgument,
|
||||
ValueReason.ReducerState,
|
||||
ValueReason.State,
|
||||
]);
|
||||
|
||||
// The effect with which a value is modified.
|
||||
export enum Effect {
|
||||
// Default value: not allowed after lifetime inference
|
||||
@@ -1756,6 +1770,10 @@ export function isUseStateType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState';
|
||||
}
|
||||
|
||||
export function isJsxType(type: Type): boolean {
|
||||
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
|
||||
}
|
||||
|
||||
export function isRefOrRefValue(id: Identifier): boolean {
|
||||
return isUseRefType(id) || isRefValueType(id);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,18 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {AliasingSignature} from '../Inference/AliasingEffects';
|
||||
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {
|
||||
Effect,
|
||||
GeneratedSource,
|
||||
Hole,
|
||||
makeDeclarationId,
|
||||
makeIdentifierId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
SourceLocation,
|
||||
SpreadPattern,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from './HIR';
|
||||
@@ -25,6 +29,7 @@ import {
|
||||
PolyType,
|
||||
PrimitiveType,
|
||||
} from './Types';
|
||||
import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema';
|
||||
|
||||
/*
|
||||
* This file exports types and defaults for JavaScript object shapes. These are
|
||||
@@ -53,13 +58,20 @@ function createAnonId(): string {
|
||||
export function addFunction(
|
||||
registry: ShapeRegistry,
|
||||
properties: Iterable<[string, BuiltInType | PolyType]>,
|
||||
fn: Omit<FunctionSignature, 'hookKind'>,
|
||||
fn: Omit<FunctionSignature, 'hookKind' | 'aliasing'> & {
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
},
|
||||
id: string | null = null,
|
||||
isConstructor: boolean = false,
|
||||
): FunctionType {
|
||||
const shapeId = id ?? createAnonId();
|
||||
const aliasing =
|
||||
fn.aliasing != null
|
||||
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
|
||||
: null;
|
||||
addShape(registry, shapeId, properties, {
|
||||
...fn,
|
||||
aliasing,
|
||||
hookKind: null,
|
||||
});
|
||||
return {
|
||||
@@ -77,11 +89,18 @@ export function addFunction(
|
||||
*/
|
||||
export function addHook(
|
||||
registry: ShapeRegistry,
|
||||
fn: FunctionSignature & {hookKind: HookKind},
|
||||
fn: Omit<FunctionSignature, 'aliasing'> & {
|
||||
hookKind: HookKind;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
},
|
||||
id: string | null = null,
|
||||
): FunctionType {
|
||||
const shapeId = id ?? createAnonId();
|
||||
addShape(registry, shapeId, [], fn);
|
||||
const aliasing =
|
||||
fn.aliasing != null
|
||||
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
|
||||
: null;
|
||||
addShape(registry, shapeId, [], {...fn, aliasing});
|
||||
return {
|
||||
kind: 'Function',
|
||||
return: fn.returnType,
|
||||
@@ -90,6 +109,129 @@ export function addHook(
|
||||
};
|
||||
}
|
||||
|
||||
function parseAliasingSignatureConfig(
|
||||
typeConfig: AliasingSignatureConfig,
|
||||
moduleName: string,
|
||||
loc: SourceLocation,
|
||||
): AliasingSignature {
|
||||
const lifetimes = new Map<string, Place>();
|
||||
function define(temp: string): Place {
|
||||
CompilerError.invariant(!lifetimes.has(temp), {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
|
||||
loc,
|
||||
});
|
||||
const place = signatureArgument(lifetimes.size);
|
||||
lifetimes.set(temp, place);
|
||||
return place;
|
||||
}
|
||||
function lookup(temp: string): Place {
|
||||
const place = lifetimes.get(temp);
|
||||
CompilerError.invariant(place != null, {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
|
||||
loc,
|
||||
});
|
||||
return place;
|
||||
}
|
||||
const receiver = define(typeConfig.receiver);
|
||||
const params = typeConfig.params.map(define);
|
||||
const rest = typeConfig.rest != null ? define(typeConfig.rest) : null;
|
||||
const returns = define(typeConfig.returns);
|
||||
const temporaries = typeConfig.temporaries.map(define);
|
||||
const effects = typeConfig.effects.map(
|
||||
(effect: AliasingEffectConfig): AliasingEffect => {
|
||||
switch (effect.kind) {
|
||||
case 'CreateFrom':
|
||||
case 'Capture':
|
||||
case 'Alias':
|
||||
case 'Assign': {
|
||||
const from = lookup(effect.from);
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: effect.kind,
|
||||
from,
|
||||
into,
|
||||
};
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
const value = lookup(effect.value);
|
||||
return {kind: effect.kind, value};
|
||||
}
|
||||
case 'Create': {
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: 'Create',
|
||||
into,
|
||||
reason: effect.reason,
|
||||
value: effect.value,
|
||||
};
|
||||
}
|
||||
case 'Freeze': {
|
||||
const value = lookup(effect.value);
|
||||
return {
|
||||
kind: 'Freeze',
|
||||
value,
|
||||
reason: effect.reason,
|
||||
};
|
||||
}
|
||||
case 'Impure': {
|
||||
const place = lookup(effect.place);
|
||||
return {
|
||||
kind: 'Impure',
|
||||
place,
|
||||
error: CompilerError.throwTodo({
|
||||
reason: 'Support impure effect declarations',
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'Apply': {
|
||||
const receiver = lookup(effect.receiver);
|
||||
const fn = lookup(effect.function);
|
||||
const args: Array<Place | SpreadPattern | Hole> = effect.args.map(
|
||||
arg => {
|
||||
if (typeof arg === 'string') {
|
||||
return lookup(arg);
|
||||
} else if (arg.kind === 'Spread') {
|
||||
return {kind: 'Spread', place: lookup(arg.place)};
|
||||
} else {
|
||||
return arg;
|
||||
}
|
||||
},
|
||||
);
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: 'Apply',
|
||||
receiver,
|
||||
function: fn,
|
||||
mutatesFunction: effect.mutatesFunction,
|
||||
args,
|
||||
into,
|
||||
loc,
|
||||
signature: null,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind '${(effect as any).kind}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
return {
|
||||
receiver: receiver.identifier.id,
|
||||
params: params.map(p => p.identifier.id),
|
||||
rest: rest != null ? rest.identifier.id : null,
|
||||
returns: returns.identifier.id,
|
||||
temporaries,
|
||||
effects,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Add an object to an existing ShapeRegistry.
|
||||
*
|
||||
@@ -192,8 +334,7 @@ export type FunctionSignature = {
|
||||
|
||||
canonicalName?: string;
|
||||
|
||||
aliasing?: AliasingSignature | null;
|
||||
todo_aliasing?: AliasingSignature | null;
|
||||
aliasing?: AliasingSignature | null | undefined;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -320,24 +461,24 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Push directly mutates the array itself
|
||||
{kind: 'Mutate', value: signatureArgument(0)},
|
||||
{kind: 'Mutate', value: '@receiver'},
|
||||
// The arguments are captured into the array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(0),
|
||||
from: '@rest',
|
||||
into: '@receiver',
|
||||
},
|
||||
// Returns the new length, a primitive
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
into: '@returns',
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
@@ -374,58 +515,56 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
noAlias: true,
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [makeIdentifierId(1)],
|
||||
receiver: '@receiver',
|
||||
params: ['@callback'],
|
||||
rest: null,
|
||||
returns: makeIdentifierId(2),
|
||||
returns: '@returns',
|
||||
temporaries: [
|
||||
// Temporary representing captured items of the receiver
|
||||
signatureArgument(3),
|
||||
'@item',
|
||||
// Temporary representing the result of the callback
|
||||
signatureArgument(4),
|
||||
'@callbackReturn',
|
||||
/*
|
||||
* Undefined `this` arg to the callback. Note the signature does not
|
||||
* support passing an explicit thisArg second param
|
||||
*/
|
||||
signatureArgument(5),
|
||||
'@thisArg',
|
||||
],
|
||||
effects: [
|
||||
// Map creates a new mutable array
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
into: '@returns',
|
||||
value: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The first arg to the callback is an item extracted from the receiver array
|
||||
{
|
||||
kind: 'CreateFrom',
|
||||
from: signatureArgument(0),
|
||||
into: signatureArgument(3),
|
||||
from: '@receiver',
|
||||
into: '@item',
|
||||
},
|
||||
// The undefined this for the callback
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(5),
|
||||
into: '@thisArg',
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// calls the callback, returning the result into a temporary
|
||||
{
|
||||
kind: 'Apply',
|
||||
receiver: signatureArgument(5),
|
||||
args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)],
|
||||
function: signatureArgument(1),
|
||||
into: signatureArgument(4),
|
||||
signature: null,
|
||||
receiver: '@thisArg',
|
||||
args: ['@item', {kind: 'Hole'}, '@receiver'],
|
||||
function: '@callback',
|
||||
into: '@callbackReturn',
|
||||
mutatesFunction: false,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
// captures the result of the callback into the return array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(4),
|
||||
into: signatureArgument(2),
|
||||
from: '@callbackReturn',
|
||||
into: '@returns',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -577,28 +716,28 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
|
||||
// returnValueKind is technically dependent on the ValueKind of the set itself
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Set.add returns the receiver Set
|
||||
{
|
||||
kind: 'Assign',
|
||||
from: signatureArgument(0),
|
||||
into: signatureArgument(2),
|
||||
from: '@receiver',
|
||||
into: '@returns',
|
||||
},
|
||||
// Set.add mutates the set itself
|
||||
{
|
||||
kind: 'Mutate',
|
||||
value: signatureArgument(0),
|
||||
value: '@receiver',
|
||||
},
|
||||
// Captures the rest params into the set
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(0),
|
||||
from: '@rest',
|
||||
into: '@receiver',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1303,30 +1442,30 @@ export const DefaultNonmutatingHook = addHook(
|
||||
hookKind: 'Custom',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Freeze the arguments
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: signatureArgument(1),
|
||||
value: '@rest',
|
||||
reason: ValueReason.HookCaptured,
|
||||
},
|
||||
// Returns a frozen value
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
into: '@returns',
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.HookReturn,
|
||||
},
|
||||
// May alias any arguments into the return
|
||||
{
|
||||
kind: 'Alias',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(2),
|
||||
from: '@rest',
|
||||
into: '@returns',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {EffectSchema, ValueKindSchema} from './HIR';
|
||||
import {
|
||||
EffectSchema,
|
||||
ValueKindSchema,
|
||||
ValueReason,
|
||||
ValueReasonSchema,
|
||||
} from './HIR';
|
||||
|
||||
export type ObjectPropertiesConfig = {[key: string]: TypeConfig};
|
||||
export const ObjectPropertiesSchema: z.ZodType<ObjectPropertiesConfig> = z
|
||||
@@ -31,6 +36,194 @@ export const ObjectTypeSchema: z.ZodType<ObjectTypeConfig> = z.object({
|
||||
properties: ObjectPropertiesSchema.nullable(),
|
||||
});
|
||||
|
||||
export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), {
|
||||
message: "Placeholder names must start with '@'",
|
||||
});
|
||||
|
||||
export type FreezeEffectConfig = {
|
||||
kind: 'Freeze';
|
||||
value: string;
|
||||
reason: ValueReason;
|
||||
};
|
||||
|
||||
export const FreezeEffectSchema: z.ZodType<FreezeEffectConfig> = z.object({
|
||||
kind: z.literal('Freeze'),
|
||||
value: LifetimeIdSchema,
|
||||
reason: ValueReasonSchema,
|
||||
});
|
||||
|
||||
export type MutateEffectConfig = {
|
||||
kind: 'Mutate';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const MutateEffectSchema: z.ZodType<MutateEffectConfig> = z.object({
|
||||
kind: z.literal('Mutate'),
|
||||
value: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type MutateTransitiveConditionallyConfig = {
|
||||
kind: 'MutateTransitiveConditionally';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const MutateTransitiveConditionallySchema: z.ZodType<MutateTransitiveConditionallyConfig> =
|
||||
z.object({
|
||||
kind: z.literal('MutateTransitiveConditionally'),
|
||||
value: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CreateEffectConfig = {
|
||||
kind: 'Create';
|
||||
into: string;
|
||||
value: ValueKind;
|
||||
reason: ValueReason;
|
||||
};
|
||||
|
||||
export const CreateEffectSchema: z.ZodType<CreateEffectConfig> = z.object({
|
||||
kind: z.literal('Create'),
|
||||
into: LifetimeIdSchema,
|
||||
value: ValueKindSchema,
|
||||
reason: ValueReasonSchema,
|
||||
});
|
||||
|
||||
export type AssignEffectConfig = {
|
||||
kind: 'Assign';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const AssignEffectSchema: z.ZodType<AssignEffectConfig> = z.object({
|
||||
kind: z.literal('Assign'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type AliasEffectConfig = {
|
||||
kind: 'Alias';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
|
||||
kind: z.literal('Alias'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CaptureEffectConfig = {
|
||||
kind: 'Capture';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const CaptureEffectSchema: z.ZodType<CaptureEffectConfig> = z.object({
|
||||
kind: z.literal('Capture'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CreateFromEffectConfig = {
|
||||
kind: 'CreateFrom';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const CreateFromEffectSchema: z.ZodType<CreateFromEffectConfig> =
|
||||
z.object({
|
||||
kind: z.literal('CreateFrom'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type ApplyArgConfig =
|
||||
| string
|
||||
| {kind: 'Spread'; place: string}
|
||||
| {kind: 'Hole'};
|
||||
|
||||
export const ApplyArgSchema: z.ZodType<ApplyArgConfig> = z.union([
|
||||
LifetimeIdSchema,
|
||||
z.object({
|
||||
kind: z.literal('Spread'),
|
||||
place: LifetimeIdSchema,
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('Hole'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type ApplyEffectConfig = {
|
||||
kind: 'Apply';
|
||||
receiver: string;
|
||||
function: string;
|
||||
mutatesFunction: boolean;
|
||||
args: Array<ApplyArgConfig>;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const ApplyEffectSchema: z.ZodType<ApplyEffectConfig> = z.object({
|
||||
kind: z.literal('Apply'),
|
||||
receiver: LifetimeIdSchema,
|
||||
function: LifetimeIdSchema,
|
||||
mutatesFunction: z.boolean(),
|
||||
args: z.array(ApplyArgSchema),
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type ImpureEffectConfig = {
|
||||
kind: 'Impure';
|
||||
place: string;
|
||||
};
|
||||
|
||||
export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
|
||||
kind: z.literal('Impure'),
|
||||
place: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type AliasingEffectConfig =
|
||||
| FreezeEffectConfig
|
||||
| CreateEffectConfig
|
||||
| CreateFromEffectConfig
|
||||
| AssignEffectConfig
|
||||
| AliasEffectConfig
|
||||
| CaptureEffectConfig
|
||||
| ImpureEffectConfig
|
||||
| MutateEffectConfig
|
||||
| MutateTransitiveConditionallyConfig
|
||||
| ApplyEffectConfig;
|
||||
|
||||
export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
|
||||
FreezeEffectSchema,
|
||||
CreateEffectSchema,
|
||||
CreateFromEffectSchema,
|
||||
AssignEffectSchema,
|
||||
AliasEffectSchema,
|
||||
CaptureEffectSchema,
|
||||
ImpureEffectSchema,
|
||||
MutateEffectSchema,
|
||||
MutateTransitiveConditionallySchema,
|
||||
ApplyEffectSchema,
|
||||
]);
|
||||
|
||||
export type AliasingSignatureConfig = {
|
||||
receiver: string;
|
||||
params: Array<string>;
|
||||
rest: string | null;
|
||||
returns: string;
|
||||
effects: Array<AliasingEffectConfig>;
|
||||
temporaries: Array<string>;
|
||||
};
|
||||
|
||||
export const AliasingSignatureSchema: z.ZodType<AliasingSignatureConfig> =
|
||||
z.object({
|
||||
receiver: LifetimeIdSchema,
|
||||
params: z.array(LifetimeIdSchema),
|
||||
rest: LifetimeIdSchema.nullable(),
|
||||
returns: LifetimeIdSchema,
|
||||
effects: z.array(AliasingEffectSchema),
|
||||
temporaries: z.array(LifetimeIdSchema),
|
||||
});
|
||||
|
||||
export type FunctionTypeConfig = {
|
||||
kind: 'function';
|
||||
positionalParams: Array<Effect>;
|
||||
@@ -42,6 +235,7 @@ export type FunctionTypeConfig = {
|
||||
mutableOnlyIfOperandsAreMutable?: boolean | null | undefined;
|
||||
impure?: boolean | null | undefined;
|
||||
canonicalName?: string | null | undefined;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
};
|
||||
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
|
||||
kind: z.literal('function'),
|
||||
@@ -54,6 +248,7 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
|
||||
mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(),
|
||||
impure: z.boolean().nullable().optional(),
|
||||
canonicalName: z.string().nullable().optional(),
|
||||
aliasing: AliasingSignatureSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export type HookTypeConfig = {
|
||||
@@ -63,6 +258,7 @@ export type HookTypeConfig = {
|
||||
returnType: TypeConfig;
|
||||
returnValueKind?: ValueKind | null | undefined;
|
||||
noAlias?: boolean | null | undefined;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
};
|
||||
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
|
||||
kind: z.literal('hook'),
|
||||
@@ -71,6 +267,7 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
|
||||
returnType: z.lazy(() => TypeSchema),
|
||||
returnValueKind: ValueKindSchema.nullable().optional(),
|
||||
noAlias: z.boolean().nullable().optional(),
|
||||
aliasing: AliasingSignatureSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export type BuiltInTypeConfig =
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
ObjectMethod,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import {printSourceLocation} from '../HIR/PrintHIR';
|
||||
|
||||
/**
|
||||
* `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or
|
||||
@@ -200,10 +202,19 @@ export function hashEffect(effect: AliasingEffect): string {
|
||||
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
|
||||
}
|
||||
case 'Impure':
|
||||
case 'Render':
|
||||
case 'Render': {
|
||||
return [effect.kind, effect.place.identifier.id].join(':');
|
||||
}
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
return [effect.kind, effect.place.identifier.id].join(':');
|
||||
return [
|
||||
effect.kind,
|
||||
effect.place.identifier.id,
|
||||
effect.error.severity,
|
||||
effect.error.reason,
|
||||
effect.error.description,
|
||||
printSourceLocation(effect.error.loc ?? GeneratedSource),
|
||||
].join(':');
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
|
||||
@@ -22,7 +22,6 @@ import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
|
||||
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
|
||||
|
||||
export default function analyseFunctions(func: HIRFunction): void {
|
||||
@@ -68,19 +67,12 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
analyseFunctions(fn);
|
||||
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
|
||||
deadCodeElimination(fn);
|
||||
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
|
||||
const functionEffects = inferMutationAliasingRanges(fn, {
|
||||
isFunctionExpression: true,
|
||||
}).unwrap();
|
||||
rewriteInstructionKindsBasedOnReassignment(fn);
|
||||
inferReactiveScopeVariables(fn);
|
||||
const effects = inferMutationAliasingFunctionEffects(fn);
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: fn,
|
||||
});
|
||||
if (effects != null) {
|
||||
fn.aliasingEffects ??= [];
|
||||
fn.aliasingEffects?.push(...effects);
|
||||
}
|
||||
fn.aliasingEffects = functionEffects;
|
||||
|
||||
/**
|
||||
* Phase 2: populate the Effect of each context variable to use in inferring
|
||||
@@ -88,7 +80,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
* effects to decide if the function may be mutable or not.
|
||||
*/
|
||||
const capturedOrMutated = new Set<IdentifierId>();
|
||||
for (const effect of effects ?? []) {
|
||||
for (const effect of functionEffects) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
@@ -140,6 +132,12 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: fn,
|
||||
});
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
import {
|
||||
eachInstructionValueLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {Ok, Result} from '../Utils/Result';
|
||||
@@ -49,6 +50,7 @@ import {
|
||||
} from './InferReferenceEffects';
|
||||
import {
|
||||
assertExhaustive,
|
||||
getOrInsertDefault,
|
||||
getOrInsertWith,
|
||||
Set_isSuperset,
|
||||
} from '../Utils/utils';
|
||||
@@ -191,16 +193,15 @@ export function inferMutationAliasingEffects(
|
||||
hoistedContextDeclarations,
|
||||
);
|
||||
|
||||
let count = 0;
|
||||
let iterationCount = 0;
|
||||
while (queuedStates.size !== 0) {
|
||||
count++;
|
||||
if (count > 1000) {
|
||||
console.log(
|
||||
'oops infinite loop',
|
||||
fn.id,
|
||||
typeof fn.loc !== 'symbol' ? fn.loc?.filename : null,
|
||||
);
|
||||
throw new Error('infinite loop');
|
||||
iterationCount++;
|
||||
if (iterationCount > 100) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[InferMutationAliasingEffects] Potential infinite loop`,
|
||||
description: `A value, temporary place, or effect was not cached properly`,
|
||||
loc: fn.loc,
|
||||
});
|
||||
}
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
const incomingState = queuedStates.get(blockId);
|
||||
@@ -221,8 +222,19 @@ export function inferMutationAliasingEffects(
|
||||
return Ok(undefined);
|
||||
}
|
||||
|
||||
function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
|
||||
const hoisted = new Set<DeclarationId>();
|
||||
function findHoistedContextDeclarations(
|
||||
fn: HIRFunction,
|
||||
): Map<DeclarationId, Place | null> {
|
||||
const hoisted = new Map<DeclarationId, Place | null>();
|
||||
function visit(place: Place): void {
|
||||
if (
|
||||
hoisted.has(place.identifier.declarationId) &&
|
||||
hoisted.get(place.identifier.declarationId) == null
|
||||
) {
|
||||
// If this is the first load of the value, store the location
|
||||
hoisted.set(place.identifier.declarationId, place);
|
||||
}
|
||||
}
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.value.kind === 'DeclareContext') {
|
||||
@@ -232,10 +244,17 @@ function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
|
||||
kind == InstructionKind.HoistedFunction ||
|
||||
kind == InstructionKind.HoistedLet
|
||||
) {
|
||||
hoisted.add(instr.value.lvalue.place.identifier.declarationId);
|
||||
hoisted.set(instr.value.lvalue.place.identifier.declarationId, null);
|
||||
}
|
||||
} else {
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
visit(operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
visit(operand);
|
||||
}
|
||||
}
|
||||
return hoisted;
|
||||
}
|
||||
@@ -245,21 +264,40 @@ class Context {
|
||||
instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
|
||||
effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
|
||||
new Map();
|
||||
applySignatureCache: Map<
|
||||
AliasingSignature,
|
||||
Map<AliasingEffect, Array<AliasingEffect> | null>
|
||||
> = new Map();
|
||||
catchHandlers: Map<BlockId, Place> = new Map();
|
||||
functionSignatureCache: Map<FunctionExpression, AliasingSignature> =
|
||||
new Map();
|
||||
isFuctionExpression: boolean;
|
||||
fn: HIRFunction;
|
||||
hoistedContextDeclarations: Set<DeclarationId>;
|
||||
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
|
||||
|
||||
constructor(
|
||||
isFunctionExpression: boolean,
|
||||
fn: HIRFunction,
|
||||
hoistedContextDeclarations: Set<DeclarationId>,
|
||||
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
|
||||
) {
|
||||
this.isFuctionExpression = isFunctionExpression;
|
||||
this.fn = fn;
|
||||
this.hoistedContextDeclarations = hoistedContextDeclarations;
|
||||
}
|
||||
|
||||
cacheApplySignature(
|
||||
signature: AliasingSignature,
|
||||
effect: Extract<AliasingEffect, {kind: 'Apply'}>,
|
||||
f: () => Array<AliasingEffect> | null,
|
||||
): Array<AliasingEffect> | null {
|
||||
const inner = getOrInsertDefault(
|
||||
this.applySignatureCache,
|
||||
signature,
|
||||
new Map(),
|
||||
);
|
||||
return getOrInsertWith(inner, effect, f);
|
||||
}
|
||||
|
||||
internEffect(effect: AliasingEffect): AliasingEffect {
|
||||
const hash = hashEffect(effect);
|
||||
let interned = this.internedEffects.get(hash);
|
||||
@@ -314,6 +352,11 @@ function inferBlock(
|
||||
} else if (terminal.kind === 'maybe-throw') {
|
||||
const handlerParam = context.catchHandlers.get(terminal.handler);
|
||||
if (handlerParam != null) {
|
||||
CompilerError.invariant(state.kind(handlerParam) != null, {
|
||||
reason:
|
||||
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
|
||||
loc: terminal.loc,
|
||||
});
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
@@ -333,11 +376,13 @@ function inferBlock(
|
||||
state.appendAlias(handlerParam, instr.lvalue);
|
||||
const kind = state.kind(instr.lvalue).kind;
|
||||
if (kind === ValueKind.Mutable || kind == ValueKind.Context) {
|
||||
effects.push({
|
||||
kind: 'Alias',
|
||||
from: instr.lvalue,
|
||||
into: handlerParam,
|
||||
});
|
||||
effects.push(
|
||||
context.internEffect({
|
||||
kind: 'Alias',
|
||||
from: instr.lvalue,
|
||||
into: handlerParam,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,11 +391,11 @@ function inferBlock(
|
||||
} else if (terminal.kind === 'return') {
|
||||
if (!context.isFuctionExpression) {
|
||||
terminal.effects = [
|
||||
{
|
||||
context.internEffect({
|
||||
kind: 'Freeze',
|
||||
value: terminal.value,
|
||||
reason: ValueReason.JsxCaptured,
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -426,14 +471,14 @@ function applySignature(
|
||||
* Track which values we've already aliased once, so that we can switch to
|
||||
* appendAlias() for subsequent aliases into the same value
|
||||
*/
|
||||
const aliased = new Set<IdentifierId>();
|
||||
const initialized = new Set<IdentifierId>();
|
||||
|
||||
if (DEBUG) {
|
||||
console.log(printInstruction(instruction));
|
||||
}
|
||||
|
||||
for (const effect of signature.effects) {
|
||||
applyEffect(context, state, effect, aliased, effects);
|
||||
applyEffect(context, state, effect, initialized, effects);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
@@ -458,7 +503,7 @@ function applyEffect(
|
||||
context: Context,
|
||||
state: InferenceState,
|
||||
_effect: AliasingEffect,
|
||||
aliased: Set<IdentifierId>,
|
||||
initialized: Set<IdentifierId>,
|
||||
effects: Array<AliasingEffect>,
|
||||
): void {
|
||||
const effect = context.internEffect(_effect);
|
||||
@@ -474,6 +519,13 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
case 'Create': {
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
if (value == null) {
|
||||
value = {
|
||||
@@ -488,6 +540,7 @@ function applyEffect(
|
||||
reason: new Set([effect.reason]),
|
||||
});
|
||||
state.define(effect.into, value);
|
||||
effects.push(effect);
|
||||
break;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
@@ -505,6 +558,13 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
case 'CreateFrom': {
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
const fromValue = state.kind(effect.from);
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
if (value == null) {
|
||||
@@ -523,29 +583,48 @@ function applyEffect(
|
||||
switch (fromValue.kind) {
|
||||
case ValueKind.Primitive:
|
||||
case ValueKind.Global: {
|
||||
// no need to track this data flow
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
value: fromValue.kind,
|
||||
into: effect.into,
|
||||
reason: [...fromValue.reason][0] ?? ValueReason.Other,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ValueKind.Frozen: {
|
||||
effects.push({
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
kind: 'Create',
|
||||
value: fromValue.kind,
|
||||
into: effect.into,
|
||||
reason: [...fromValue.reason][0] ?? ValueReason.Other,
|
||||
});
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
effects.push({
|
||||
// OK: recording information flow
|
||||
kind: 'CreateFrom', // prev Alias
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
});
|
||||
effects.push(effect);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CreateFunction': {
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
effects.push(effect);
|
||||
/**
|
||||
* We consider the function mutable if it has any mutable context variables or
|
||||
@@ -602,7 +681,7 @@ function applyEffect(
|
||||
from: capture,
|
||||
into: effect.into,
|
||||
},
|
||||
aliased,
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
@@ -610,6 +689,14 @@ function applyEffect(
|
||||
}
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
|
||||
{
|
||||
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
|
||||
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
|
||||
loc: effect.into.loc,
|
||||
},
|
||||
);
|
||||
/*
|
||||
* Capture describes potential information flow: storing a pointer to one value
|
||||
* within another. If the destination is not mutable, or the source value has
|
||||
@@ -639,11 +726,17 @@ function applyEffect(
|
||||
}
|
||||
case ValueKind.Frozen: {
|
||||
isMutableReferenceType = false;
|
||||
effects.push({
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
});
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -657,6 +750,13 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
case 'Assign': {
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
/*
|
||||
* Alias represents potential pointer aliasing. If the type is a global,
|
||||
* a primitive (copy-on-write semantics) then we can prune the effect
|
||||
@@ -665,11 +765,17 @@ function applyEffect(
|
||||
const fromKind = fromValue.kind;
|
||||
switch (fromKind) {
|
||||
case ValueKind.Frozen: {
|
||||
effects.push({
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
});
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
let value = context.effectInstructionValueCache.get(effect);
|
||||
if (value == null) {
|
||||
value = {
|
||||
@@ -705,12 +811,7 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (aliased.has(effect.into.identifier.id)) {
|
||||
state.appendAlias(effect.into, effect.from);
|
||||
} else {
|
||||
aliased.add(effect.into.identifier.id);
|
||||
state.alias(effect.into, effect.from);
|
||||
}
|
||||
state.assign(effect.into, effect.from);
|
||||
effects.push(effect);
|
||||
break;
|
||||
}
|
||||
@@ -721,70 +822,78 @@ function applyEffect(
|
||||
const functionValues = state.values(effect.function);
|
||||
if (
|
||||
functionValues.length === 1 &&
|
||||
functionValues[0].kind === 'FunctionExpression'
|
||||
functionValues[0].kind === 'FunctionExpression' &&
|
||||
functionValues[0].loweredFunc.func.aliasingEffects != null
|
||||
) {
|
||||
/*
|
||||
* We're calling a locally declared function, we already know it's effects!
|
||||
* We just have to substitute in the args for the params
|
||||
*/
|
||||
const signature = buildSignatureFromFunctionExpression(
|
||||
state.env,
|
||||
functionValues[0],
|
||||
);
|
||||
const functionExpr = functionValues[0];
|
||||
let signature = context.functionSignatureCache.get(functionExpr);
|
||||
if (signature == null) {
|
||||
signature = buildSignatureFromFunctionExpression(
|
||||
state.env,
|
||||
functionExpr,
|
||||
);
|
||||
context.functionSignatureCache.set(functionExpr, signature);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`constructed alias signature:\n${printAliasingSignature(signature)}`,
|
||||
);
|
||||
}
|
||||
const signatureEffects = computeEffectsForSignature(
|
||||
state.env,
|
||||
const signatureEffects = context.cacheApplySignature(
|
||||
signature,
|
||||
effect.into,
|
||||
effect.receiver,
|
||||
effect.args,
|
||||
functionValues[0].loweredFunc.func.context,
|
||||
effect.loc,
|
||||
effect,
|
||||
() =>
|
||||
computeEffectsForSignature(
|
||||
state.env,
|
||||
signature,
|
||||
effect.into,
|
||||
effect.receiver,
|
||||
effect.args,
|
||||
functionExpr.loweredFunc.func.context,
|
||||
effect.loc,
|
||||
),
|
||||
);
|
||||
if (signatureEffects != null) {
|
||||
if (DEBUG) {
|
||||
console.log('apply function expression effects');
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{kind: 'MutateTransitiveConditionally', value: effect.function},
|
||||
aliased,
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
for (const signatureEffect of signatureEffects) {
|
||||
applyEffect(context, state, signatureEffect, aliased, effects);
|
||||
applyEffect(context, state, signatureEffect, initialized, effects);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
const signatureEffects =
|
||||
effect.signature?.aliasing != null
|
||||
? computeEffectsForSignature(
|
||||
let signatureEffects = null;
|
||||
if (effect.signature?.aliasing != null) {
|
||||
const signature = effect.signature.aliasing;
|
||||
signatureEffects = context.cacheApplySignature(
|
||||
effect.signature.aliasing,
|
||||
effect,
|
||||
() =>
|
||||
computeEffectsForSignature(
|
||||
state.env,
|
||||
effect.signature.aliasing,
|
||||
signature,
|
||||
effect.into,
|
||||
effect.receiver,
|
||||
effect.args,
|
||||
[],
|
||||
effect.loc,
|
||||
)
|
||||
: null;
|
||||
),
|
||||
);
|
||||
}
|
||||
if (signatureEffects != null) {
|
||||
if (DEBUG) {
|
||||
console.log('apply aliasing signature effects');
|
||||
}
|
||||
for (const signatureEffect of signatureEffects) {
|
||||
applyEffect(context, state, signatureEffect, aliased, effects);
|
||||
applyEffect(context, state, signatureEffect, initialized, effects);
|
||||
}
|
||||
} else if (effect.signature != null) {
|
||||
if (DEBUG) {
|
||||
console.log('apply legacy signature effects');
|
||||
}
|
||||
const legacyEffects = computeEffectsForLegacySignature(
|
||||
state,
|
||||
effect.signature,
|
||||
@@ -794,12 +903,9 @@ function applyEffect(
|
||||
effect.loc,
|
||||
);
|
||||
for (const legacyEffect of legacyEffects) {
|
||||
applyEffect(context, state, legacyEffect, aliased, effects);
|
||||
applyEffect(context, state, legacyEffect, initialized, effects);
|
||||
}
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
console.log('default effects');
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -809,7 +915,7 @@ function applyEffect(
|
||||
value: ValueKind.Mutable,
|
||||
reason: ValueReason.Other,
|
||||
},
|
||||
aliased,
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
/*
|
||||
@@ -832,21 +938,21 @@ function applyEffect(
|
||||
kind: 'MutateTransitiveConditionally',
|
||||
value: operand,
|
||||
},
|
||||
aliased,
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
const mutateIterator =
|
||||
arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null;
|
||||
if (mutateIterator) {
|
||||
applyEffect(context, state, mutateIterator, aliased, effects);
|
||||
applyEffect(context, state, mutateIterator, initialized, effects);
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
// OK: recording information flow
|
||||
{kind: 'Alias', from: operand, into: effect.into},
|
||||
aliased,
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
for (const otherArg of [
|
||||
@@ -874,7 +980,7 @@ function applyEffect(
|
||||
from: operand,
|
||||
into: other,
|
||||
},
|
||||
aliased,
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
@@ -901,48 +1007,89 @@ function applyEffect(
|
||||
console.log(prettyFormat(state.debugAbstractValue(value)));
|
||||
}
|
||||
|
||||
let reason: string;
|
||||
let description: string | null = null;
|
||||
|
||||
if (
|
||||
mutationKind === 'mutate-frozen' &&
|
||||
context.hoistedContextDeclarations.has(
|
||||
effect.value.identifier.declarationId,
|
||||
)
|
||||
) {
|
||||
reason = `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`;
|
||||
if (
|
||||
const description =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
) {
|
||||
description = `Move the declaration of \`${effect.value.identifier.name.value}\` to before it is first referenced`;
|
||||
? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared`
|
||||
: null;
|
||||
const hoistedAccess = context.hoistedContextDeclarations.get(
|
||||
effect.value.identifier.declarationId,
|
||||
);
|
||||
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`,
|
||||
description,
|
||||
loc: hoistedAccess.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
|
||||
description,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
} else {
|
||||
reason = getWriteErrorReason({
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
if (
|
||||
const description =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
) {
|
||||
description = `Found mutation of \`${effect.value.identifier.name.value}\``;
|
||||
}
|
||||
? `Found mutation of \`${effect.value.identifier.name.value}\``
|
||||
: null;
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind:
|
||||
value.kind === ValueKind.Frozen
|
||||
? 'MutateFrozen'
|
||||
: 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
|
||||
effects.push({
|
||||
kind:
|
||||
value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: {
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description,
|
||||
loc: effect.value.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1046,7 +1193,7 @@ class InferenceState {
|
||||
}
|
||||
|
||||
// Updates the value at @param place to point to the same value as @param value.
|
||||
alias(place: Place, value: Place): void {
|
||||
assign(place: Place, value: Place): void {
|
||||
const values = this.#variables.get(value.identifier.id);
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
|
||||
@@ -1124,9 +1271,6 @@ class InferenceState {
|
||||
kind: ValueKind.Frozen,
|
||||
reason: new Set([reason]),
|
||||
});
|
||||
if (DEBUG) {
|
||||
console.log(`freeze value: ${printInstructionValue(value)} ${reason}`);
|
||||
}
|
||||
if (
|
||||
value.kind === 'FunctionExpression' &&
|
||||
(this.env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
@@ -1983,8 +2127,6 @@ function computeEffectsForLegacySignature(
|
||||
const mutateIterator = conditionallyMutateIterator(place);
|
||||
if (mutateIterator != null) {
|
||||
effects.push(mutateIterator);
|
||||
// TODO: should we always push to captures?
|
||||
captures.push(place);
|
||||
}
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
@@ -2166,17 +2308,6 @@ function computeEffectsForSignature(
|
||||
// Too many args and there is no rest param to hold them
|
||||
(args.length > signature.params.length && signature.rest == null)
|
||||
) {
|
||||
if (DEBUG) {
|
||||
if (signature.params.length > args.length) {
|
||||
console.log(
|
||||
`not enough args: ${args.length} args for ${signature.params.length} params`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`too many args: ${args.length} args for ${signature.params.length} params, with no rest param`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Build substitutions
|
||||
@@ -2191,9 +2322,6 @@ function computeEffectsForSignature(
|
||||
continue;
|
||||
} else if (params == null || i >= params.length || arg.kind === 'Spread') {
|
||||
if (signature.rest == null) {
|
||||
if (DEBUG) {
|
||||
console.log(`no rest value to hold param`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
@@ -2301,23 +2429,14 @@ function computeEffectsForSignature(
|
||||
case 'Apply': {
|
||||
const applyReceiver = substitutions.get(effect.receiver.identifier.id);
|
||||
if (applyReceiver == null || applyReceiver.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for receiver`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const applyFunction = substitutions.get(effect.function.identifier.id);
|
||||
if (applyFunction == null || applyFunction.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for function`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const applyInto = substitutions.get(effect.into.identifier.id);
|
||||
if (applyInto == null || applyInto.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for into`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const applyArgs: Array<Place | SpreadPattern | Hole> = [];
|
||||
@@ -2327,18 +2446,12 @@ function computeEffectsForSignature(
|
||||
} else if (arg.kind === 'Identifier') {
|
||||
const applyArg = substitutions.get(arg.identifier.id);
|
||||
if (applyArg == null || applyArg.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for arg`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
applyArgs.push(applyArg[0]);
|
||||
} else {
|
||||
const applyArg = substitutions.get(arg.place.identifier.id);
|
||||
if (applyArg == null || applyArg.length !== 1) {
|
||||
if (DEBUG) {
|
||||
console.log(`too many substitutions for arg`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
applyArgs.push({kind: 'Spread', place: applyArg[0]});
|
||||
|
||||
@@ -1,206 +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.
|
||||
*/
|
||||
|
||||
import {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
import {AliasingEffect} from './AliasingEffects';
|
||||
|
||||
/**
|
||||
* This function tracks data flow within an inner function expression in order to
|
||||
* compute a set of data-flow aliasing effects describing data flow between the function's
|
||||
* params, context variables, and return value.
|
||||
*
|
||||
* For example, consider the following function expression:
|
||||
*
|
||||
* ```
|
||||
* (x) => { return [x, y] }
|
||||
* ```
|
||||
*
|
||||
* This function captures both param `x` and context variable `y` into the return value.
|
||||
* Unlike our previous inference which counted this as a mutation of x and y, we want to
|
||||
* build a signature for the function that describes the data flow. We would infer
|
||||
* `Capture x -> return, Capture y -> return` effects for this function.
|
||||
*
|
||||
* This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render)
|
||||
* from instructions within the function up to the function itself.
|
||||
*/
|
||||
export function inferMutationAliasingFunctionEffects(
|
||||
fn: HIRFunction,
|
||||
): Array<AliasingEffect> | null {
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
|
||||
/**
|
||||
* Map used to identify tracked variables: params, context vars, return value
|
||||
* This is used to detect mutation/capturing/aliasing of params/context vars
|
||||
*/
|
||||
const tracked = new Map<IdentifierId, Place>();
|
||||
tracked.set(fn.returns.identifier.id, fn.returns);
|
||||
for (const operand of [...fn.context, ...fn.params]) {
|
||||
const place = operand.kind === 'Identifier' ? operand : operand.place;
|
||||
tracked.set(place.identifier.id, place);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track capturing/aliasing of context vars and params into each other and into the return.
|
||||
* We don't need to track locals and intermediate values, since we're only concerned with effects
|
||||
* as they relate to arguments visible outside the function.
|
||||
*
|
||||
* For each aliased identifier we track capture/alias/createfrom and then merge this with how
|
||||
* the value is used. Eg capturing an alias => capture. See joinEffects() helper.
|
||||
*/
|
||||
type AliasedIdentifier = {
|
||||
kind: AliasingKind;
|
||||
place: Place;
|
||||
};
|
||||
const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
|
||||
|
||||
/*
|
||||
* Check for aliasing of tracked values. Also joins the effects of how the value is
|
||||
* used (@param kind) with the aliasing type of each value
|
||||
*/
|
||||
function lookup(
|
||||
place: Place,
|
||||
kind: AliasedIdentifier['kind'],
|
||||
): Array<AliasedIdentifier> | null {
|
||||
if (tracked.has(place.identifier.id)) {
|
||||
return [{kind, place}];
|
||||
}
|
||||
return (
|
||||
dataFlow.get(place.identifier.id)?.map(aliased => ({
|
||||
kind: joinEffects(aliased.kind, kind),
|
||||
place: aliased.place,
|
||||
})) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
// todo: fixpoint
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
const operands: Array<AliasedIdentifier> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
const inputs = lookup(operand, 'Alias');
|
||||
if (inputs != null) {
|
||||
operands.push(...inputs);
|
||||
}
|
||||
}
|
||||
if (operands.length !== 0) {
|
||||
dataFlow.set(phi.place.identifier.id, operands);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (
|
||||
effect.kind === 'Assign' ||
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'Alias' ||
|
||||
effect.kind === 'CreateFrom'
|
||||
) {
|
||||
const from = lookup(effect.from, effect.kind);
|
||||
if (from == null) {
|
||||
continue;
|
||||
}
|
||||
const into = lookup(effect.into, 'Alias');
|
||||
if (into == null) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
} else {
|
||||
for (const aliased of into) {
|
||||
getOrInsertDefault(
|
||||
dataFlow,
|
||||
aliased.place.identifier.id,
|
||||
[],
|
||||
).push(...from);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
effect.kind === 'Create' ||
|
||||
effect.kind === 'CreateFunction'
|
||||
) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, [
|
||||
{kind: 'Alias', place: effect.into},
|
||||
]);
|
||||
} else if (
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure' ||
|
||||
effect.kind === 'Render'
|
||||
) {
|
||||
effects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
const from = lookup(block.terminal.value, 'Alias');
|
||||
if (from != null) {
|
||||
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create aliasing effects based on observed data flow
|
||||
let hasReturn = false;
|
||||
for (const [into, from] of dataFlow) {
|
||||
const input = tracked.get(into);
|
||||
if (input == null) {
|
||||
continue;
|
||||
}
|
||||
for (const aliased of from) {
|
||||
if (
|
||||
aliased.place.identifier.id === input.identifier.id ||
|
||||
!tracked.has(aliased.place.identifier.id)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const effect = {kind: aliased.kind, from: aliased.place, into: input};
|
||||
effects.push(effect);
|
||||
if (
|
||||
into === fn.returns.identifier.id &&
|
||||
(aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
|
||||
) {
|
||||
hasReturn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: more precise return effect inference
|
||||
if (!hasReturn) {
|
||||
effects.unshift({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value:
|
||||
fn.returnType.kind === 'Primitive'
|
||||
? ValueKind.Primitive
|
||||
: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
});
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
export enum MutationKind {
|
||||
None = 0,
|
||||
Conditional = 1,
|
||||
Definite = 2,
|
||||
}
|
||||
|
||||
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
|
||||
function joinEffects(
|
||||
effect1: AliasingKind,
|
||||
effect2: AliasingKind,
|
||||
): AliasingKind {
|
||||
if (effect1 === 'Capture' || effect2 === 'Capture') {
|
||||
return 'Capture';
|
||||
} else if (effect1 === 'Assign' || effect2 === 'Assign') {
|
||||
return 'Assign';
|
||||
} else {
|
||||
return 'Alias';
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
@@ -14,7 +13,10 @@ import {
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
isJsxType,
|
||||
makeInstructionId,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
@@ -23,43 +25,58 @@ import {
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {printFunction} from '../HIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {MutationKind} from './InferMutationAliasingFunctionEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
const DEBUG = false;
|
||||
const VERBOSE = false;
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {AliasingEffect} from './AliasingEffects';
|
||||
|
||||
/**
|
||||
* Infers mutable ranges for all values in the program, using previously inferred
|
||||
* mutation/aliasing effects. This pass builds a data flow graph using the effects,
|
||||
* tracking an abstract notion of "when" each effect occurs relative to the others.
|
||||
* It then walks each mutation effect against the graph, updating the range of each
|
||||
* node that would be reachable at the "time" that the effect occurred.
|
||||
* This pass builds an abstract model of the heap and interprets the effects of the
|
||||
* given function in order to determine the following:
|
||||
* - The mutable ranges of all identifiers in the function
|
||||
* - The externally-visible effects of the function, such as mutations of params and
|
||||
* context-vars, aliasing between params/context-vars/return-value, and impure side
|
||||
* effects.
|
||||
* - The legacy `Effect` to store on each Place.
|
||||
*
|
||||
* This pass builds a data flow graph using the effects, tracking an abstract notion
|
||||
* of "when" each effect occurs relative to the others. It then walks each mutation
|
||||
* effect against the graph, updating the range of each node that would be reachable
|
||||
* at the "time" that the effect occurred.
|
||||
*
|
||||
* This pass also validates against invalid effects: any function that is reachable
|
||||
* by being called, or via a Render effect, is validated against mutating globals
|
||||
* or calling impure code.
|
||||
*
|
||||
* Note that this function also populates the outer function's aliasing effects with
|
||||
* any mutations that apply to its params or context variables. For example, a
|
||||
* function expression such as the following:
|
||||
* any mutations that apply to its params or context variables.
|
||||
*
|
||||
* ## Example
|
||||
* A function expression such as the following:
|
||||
*
|
||||
* ```
|
||||
* (x) => { x.y = true }
|
||||
* ```
|
||||
*
|
||||
* Would populate a `Mutate x` aliasing effect on the outer function.
|
||||
*
|
||||
* ## Returned Function Effects
|
||||
*
|
||||
* The function returns (if successful) a list of externally-visible effects.
|
||||
* This is determined by simulating a conditional, transitive mutation against
|
||||
* each param, context variable, and return value in turn, and seeing which other
|
||||
* such values are affected. If they're affected, they must be captured, so we
|
||||
* record a Capture.
|
||||
*
|
||||
* The only tricky bit is the return value, which could _alias_ (or even assign)
|
||||
* one or more of the params/context-vars rather than just capturing. So we have
|
||||
* to do a bit more tracking for returns.
|
||||
*/
|
||||
export function inferMutationAliasingRanges(
|
||||
fn: HIRFunction,
|
||||
{isFunctionExpression}: {isFunctionExpression: boolean},
|
||||
): Result<void, CompilerError> {
|
||||
if (VERBOSE) {
|
||||
console.log();
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
): Result<Array<AliasingEffect>, CompilerError> {
|
||||
// The set of externally-visible effects
|
||||
const functionEffects: Array<AliasingEffect> = [];
|
||||
|
||||
/**
|
||||
* Part 1: Infer mutable ranges for values. We build an abstract model of
|
||||
* values, the alias/capture edges between them, and the set of mutations.
|
||||
@@ -115,20 +132,6 @@ export function inferMutationAliasingRanges(
|
||||
seenBlocks.add(block.id);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
state.create(instr.lvalue, {
|
||||
kind: 'Function',
|
||||
function: instr.value.loweredFunc.func,
|
||||
});
|
||||
} else {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
state.create(lvalue, {kind: 'Object'});
|
||||
}
|
||||
}
|
||||
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (effect.kind === 'Create') {
|
||||
@@ -141,6 +144,15 @@ export function inferMutationAliasingRanges(
|
||||
} else if (effect.kind === 'CreateFrom') {
|
||||
state.createFrom(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Assign') {
|
||||
/**
|
||||
* TODO: Invariant that the node is not initialized yet
|
||||
*
|
||||
* InferFunctionExpressionAliasingEffectSignatures currently infers
|
||||
* Assign effects in some places that should be Alias, leading to
|
||||
* Assign effects that reinitialize a value. The end result appears to
|
||||
* be fine, but we should fix that inference pass so that we add the
|
||||
* invariant here.
|
||||
*/
|
||||
if (!state.nodes.has(effect.into.identifier)) {
|
||||
state.create(effect.into, {kind: 'Object'});
|
||||
}
|
||||
@@ -183,8 +195,10 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.push(effect.error);
|
||||
functionEffects.push(effect);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
functionEffects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,10 +230,6 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
console.log(state.debug());
|
||||
console.log(pretty(mutations));
|
||||
}
|
||||
for (const mutation of mutations) {
|
||||
state.mutate(
|
||||
mutation.index,
|
||||
@@ -234,10 +244,6 @@ export function inferMutationAliasingRanges(
|
||||
for (const render of renders) {
|
||||
state.render(render.index, render.place.identifier, errors);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(pretty([...state.nodes.keys()]));
|
||||
}
|
||||
fn.aliasingEffects ??= [];
|
||||
for (const param of [...fn.context, ...fn.params]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
const node = state.nodes.get(place.identifier);
|
||||
@@ -248,13 +254,13 @@ export function inferMutationAliasingRanges(
|
||||
if (node.local != null) {
|
||||
if (node.local.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
functionEffects.push({
|
||||
kind: 'MutateConditionally',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
} else if (node.local.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
functionEffects.push({
|
||||
kind: 'Mutate',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
@@ -263,13 +269,13 @@ export function inferMutationAliasingRanges(
|
||||
if (node.transitive != null) {
|
||||
if (node.transitive.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
functionEffects.push({
|
||||
kind: 'MutateTransitiveConditionally',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
} else if (node.transitive.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
functionEffects.push({
|
||||
kind: 'MutateTransitive',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
@@ -458,10 +464,82 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
console.log(printFunction(fn));
|
||||
/**
|
||||
* Part 3
|
||||
* Finish populating the externally visible effects. Above we bubble-up the side effects
|
||||
* (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables.
|
||||
* Here we populate an effect to create the return value as well as populating alias/capture
|
||||
* effects for how data flows between the params, context vars, and return.
|
||||
*/
|
||||
functionEffects.push({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value:
|
||||
fn.returnType.kind === 'Primitive'
|
||||
? ValueKind.Primitive
|
||||
: isJsxType(fn.returnType)
|
||||
? ValueKind.Frozen
|
||||
: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
});
|
||||
/**
|
||||
* Determine precise data-flow effects by simulating transitive mutations of the params/
|
||||
* captures and seeing what other params/context variables are affected. Anything that
|
||||
* would be transitively mutated needs a capture relationship.
|
||||
*/
|
||||
const tracked: Array<Place> = [];
|
||||
const ignoredErrors = new CompilerError();
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
tracked.push(place);
|
||||
}
|
||||
return errors.asResult();
|
||||
for (const into of tracked) {
|
||||
const mutationIndex = index++;
|
||||
state.mutate(
|
||||
mutationIndex,
|
||||
into.identifier,
|
||||
null,
|
||||
true,
|
||||
MutationKind.Conditional,
|
||||
into.loc,
|
||||
ignoredErrors,
|
||||
);
|
||||
for (const from of tracked) {
|
||||
if (
|
||||
from.identifier.id === into.identifier.id ||
|
||||
from.identifier.id === fn.returns.identifier.id
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const fromNode = state.nodes.get(from.identifier);
|
||||
CompilerError.invariant(fromNode != null, {
|
||||
reason: `Expected a node to exist for all parameters and context variables`,
|
||||
loc: into.loc,
|
||||
});
|
||||
if (fromNode.lastMutated === mutationIndex) {
|
||||
if (into.identifier.id === fn.returns.identifier.id) {
|
||||
// The return value could be any of the params/context variables
|
||||
functionEffects.push({
|
||||
kind: 'Alias',
|
||||
from,
|
||||
into,
|
||||
});
|
||||
} else {
|
||||
// Otherwise params/context-vars can only capture each other
|
||||
functionEffects.push({
|
||||
kind: 'Capture',
|
||||
from,
|
||||
into,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasErrors() && !isFunctionExpression) {
|
||||
return Err(errors);
|
||||
}
|
||||
return Ok(functionEffects);
|
||||
}
|
||||
|
||||
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
@@ -477,6 +555,12 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
export enum MutationKind {
|
||||
None = 0,
|
||||
Conditional = 1,
|
||||
Definite = 2,
|
||||
}
|
||||
|
||||
type Node = {
|
||||
id: Identifier;
|
||||
createdFrom: Map<Identifier, number>;
|
||||
@@ -485,6 +569,7 @@ type Node = {
|
||||
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
|
||||
transitive: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
local: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
lastMutated: number;
|
||||
value:
|
||||
| {kind: 'Object'}
|
||||
| {kind: 'Phi'}
|
||||
@@ -502,6 +587,7 @@ class AliasingState {
|
||||
edges: [],
|
||||
transitive: null,
|
||||
local: null,
|
||||
lastMutated: 0,
|
||||
value,
|
||||
});
|
||||
}
|
||||
@@ -511,11 +597,6 @@ class AliasingState {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
@@ -528,11 +609,6 @@ class AliasingState {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
|
||||
@@ -545,11 +621,6 @@ class AliasingState {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
@@ -598,17 +669,13 @@ class AliasingState {
|
||||
mutate(
|
||||
index: number,
|
||||
start: Identifier,
|
||||
end: InstructionId,
|
||||
// Null is used for simulated mutations
|
||||
end: InstructionId | null,
|
||||
transitive: boolean,
|
||||
kind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`mutate ix=${index} start=$${start.id} end=[${end}]${transitive ? ' transitive' : ''} kind=${kind}`,
|
||||
);
|
||||
}
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<{
|
||||
place: Identifier;
|
||||
@@ -623,21 +690,14 @@ class AliasingState {
|
||||
seen.add(current);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null) {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
` mutate $${node.id.id} transitive=${transitive} direction=${direction}`,
|
||||
node.lastMutated = Math.max(node.lastMutated, index);
|
||||
if (end != null) {
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
Math.max(node.id.mutableRange.end, end),
|
||||
);
|
||||
}
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
Math.max(node.id.mutableRange.end, end),
|
||||
);
|
||||
if (
|
||||
node.value.kind === 'Function' &&
|
||||
node.transitive == null &&
|
||||
@@ -701,37 +761,5 @@ class AliasingState {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
const nodes = new Map();
|
||||
for (const id of seen) {
|
||||
const node = this.nodes.get(id);
|
||||
nodes.set(id.id, node);
|
||||
}
|
||||
console.log(pretty(nodes));
|
||||
}
|
||||
}
|
||||
|
||||
debug(): string {
|
||||
return pretty(this.nodes);
|
||||
}
|
||||
}
|
||||
|
||||
export function pretty(v: any): string {
|
||||
return prettyFormat(v, {
|
||||
plugins: [
|
||||
{
|
||||
test: v =>
|
||||
v !== null && typeof v === 'object' && v.kind === 'Identifier',
|
||||
serialize: v => printPlace(v),
|
||||
},
|
||||
{
|
||||
test: v =>
|
||||
v !== null &&
|
||||
typeof v === 'object' &&
|
||||
typeof v.declarationId === 'number',
|
||||
serialize: v =>
|
||||
`${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
# The Mutability & Aliasing Model
|
||||
|
||||
This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes).
|
||||
|
||||
To build an intuition, consider the following example:
|
||||
|
||||
```js
|
||||
function Component() {
|
||||
// a is created and mutated over the course of these two instructions:
|
||||
const a = {};
|
||||
mutate(a);
|
||||
|
||||
// b and c are created and mutated together — mutate might modify b via c
|
||||
const b = {};
|
||||
const c = {b};
|
||||
mutate(c);
|
||||
|
||||
// does not modify a/b/c
|
||||
return <Foo a={a} c={c} />
|
||||
}
|
||||
```
|
||||
|
||||
The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c.
|
||||
|
||||
In code, the mutability and aliasing model is compromised of the following phases:
|
||||
|
||||
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
|
||||
* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values.
|
||||
* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values.
|
||||
|
||||
Finally, `AnalyzeFunctions` needs to understand the mutation and aliasing semantics of nested FunctionExpression and ObjectMethod values. `AnalyzeFunctions` calls `InferFunctionExpressionAliasingEffectsSignature` to determine the publicly observable set of mutation/aliasing effects for nested functions.
|
||||
|
||||
## Mutation and Aliasing Effects
|
||||
|
||||
The inference model is based on a set of "effects" that describe subtle aspects of mutation, aliasing, and other changes to the state of values over time
|
||||
|
||||
### Creation Effects
|
||||
|
||||
#### Create
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Create';
|
||||
into: Place;
|
||||
value: ValueKind;
|
||||
reason: ValueReason;
|
||||
}
|
||||
```
|
||||
|
||||
Describes the creation of a new value with the given kind, and reason for having that kind. For example, `x = 10` might have an effect like `Create x = ValueKind.Primitive [ValueReason.Other]`.
|
||||
|
||||
#### CreateFunction
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'CreateFunction';
|
||||
captures: Array<Place>;
|
||||
function: FunctionExpression | ObjectMethod;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Describes the creation of new function value, capturing the given set of mutable values. CreateFunction is used to specifically track function types so that we can precisely model calls to those functions with `Apply`.
|
||||
|
||||
#### Apply
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Apply';
|
||||
receiver: Place;
|
||||
function: Place; // same as receiver for function calls
|
||||
mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default
|
||||
args: Array<Place | SpreadPattern | Hole>;
|
||||
into: Place; // where result is stored
|
||||
signature: FunctionSignature | null;
|
||||
}
|
||||
```
|
||||
|
||||
Describes the potential creation of a value by calling a function. This models `new`, function calls, and method calls. The inference algorithm uses the most precise signature it can determine:
|
||||
|
||||
* If the function is a locally created function expression, we use a signature inferred from the behavior of that function to interpret the effects of calling it with the given arguments.
|
||||
* Else if the function has a known aliasing signature (new style precise effects signature), we apply the arguments to that signature to get a precise set of effects.
|
||||
* Else if the function has a legacy style signature (with per-param effects) we convert the legacy per-Place effects into aliasing effects (described in this doc) and apply those.
|
||||
* Else fall back to inferring a generic set of effects.
|
||||
|
||||
The generic fallback is to assume:
|
||||
- The return value may alias any of the arguments (Alias param -> return)
|
||||
- Any arguments *may* be transitively mutated (MutateTransitiveConditionally param)
|
||||
- Any argument may be captured into any other argument (Capture paramN -> paramM for all N,M where N != M)
|
||||
|
||||
### Aliasing Effects
|
||||
|
||||
These effects describe data-flow only, separately from mutation or other state-changing semantics.
|
||||
|
||||
#### Assign
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Assign';
|
||||
from: Place;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Describes an `x = y` assignment, where the receiving (into) value is overwritten with a new (from) value. After this effect, any previous assignments/aliases to the receiving value are dropped. Note that `Alias` initializes the receiving value.
|
||||
|
||||
> TODO: InferMutationAliasingRanges may not fully reset aliases on encountering this effect
|
||||
|
||||
#### Alias
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Alias';
|
||||
from: Place;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Describes that an assignment _may_ occur, but that the possible assignment is non-exclusive. The canonical use-case for `Alias` is a function that may return more than one of its arguments, such as `(x, y, z) => x ? y : z`. Here, the result of this function may be `y` or `z`, but neither one overwrites the other. Note that `Alias` does _not_ initialize the receiving value: it should always be paired with an effect to create the receiving value.
|
||||
|
||||
#### Capture
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Capture';
|
||||
from: Place;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Describes that a reference to one variable (from) is stored within another value (into). Examples include:
|
||||
- An array expression captures the items of the array (`array = [capturedValue]`)
|
||||
- Array.prototype.push captures the pushed values into the array (`array.push(capturedValue)`)
|
||||
- Property assignment captures the value onto the object (`object.property = capturedValue`)
|
||||
|
||||
#### CreateFrom
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'CreateFrom';
|
||||
from: Place;
|
||||
into: Place;
|
||||
}
|
||||
```
|
||||
|
||||
This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes that a variable is initialized by extracting _part_ of another value, without taking a direct alias to the full other value. Examples include:
|
||||
|
||||
- Indexing into an array (`createdFrom = array[0]`)
|
||||
- Reading an object property (`createdFrom = object.property`)
|
||||
- Getting a Map key (`createdFrom = map.get(key)`)
|
||||
|
||||
#### ImmutableCapture
|
||||
|
||||
Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis.
|
||||
|
||||
### State-Changing Effects
|
||||
|
||||
The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`).
|
||||
|
||||
#### Freeze
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Freeze',
|
||||
// The reference being frozen
|
||||
value: Place;
|
||||
// The reason the value is frozen (passed to a hook, passed to jsx, etc)
|
||||
reason: ValueReason;
|
||||
}
|
||||
```
|
||||
|
||||
Once a reference to a value has been passed to React, that value is generally not safe to mutate further. This is not a strictly required property of React, but is a natural consequence of making components and hooks composable without leaking implementation details. Concretely, once a value has been passed as a JSX prop, passed as argument to a hook, or returned from a hook, it must be assumed that the other "side" — receiver of the prop/argument/return value — will use that value as an input to an effect or memoization unit. Mutating that value (instead of creating a new value) will fail to cause the consuming computation to update:
|
||||
|
||||
```js
|
||||
// INVALID DO NOT DO THIS
|
||||
function Component(props) {
|
||||
const array = useArray(props.value);
|
||||
// OOPS! this value is memoized, the array won't get re-created
|
||||
// when `props.value` changes, so we might just keep pushing new
|
||||
// values to the same array on every render!
|
||||
array.push(props.otherValue);
|
||||
}
|
||||
|
||||
function useArray(a) {
|
||||
return useMemo(() => [a], [a]);
|
||||
}
|
||||
```
|
||||
|
||||
The **Freeze** effect accepts a variable reference and a reason that the value is being frozen. Note: _freeze only applies to the reference, not the underlying value_. Our inference is conservative, and assumes that there may still be other references to the same underlying value which are mutated later. For example:
|
||||
|
||||
```js
|
||||
const x = {};
|
||||
const y = [];
|
||||
x.y = y;
|
||||
freeze(y); // y _reference_ is frozen
|
||||
x.y.push(props.value); // but y is still considered mutable bc of this
|
||||
```
|
||||
|
||||
#### Mutate (and MutateConditionally)
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'Mutate';
|
||||
value: Place;
|
||||
}
|
||||
```
|
||||
|
||||
Mutate indicates that a value is mutated, without modifying any of the values that it may transitively have captured. Canonical examples include:
|
||||
|
||||
- Pushing an item onto an array modifies the array, but does not modify any items stored _within_ the array (unless the array has a reference to itself!)
|
||||
- Assigning a value to an object property modifies the object, but not any values stored in the object's other properties.
|
||||
|
||||
This helps explain the distinction between Assign/Alias and Capture: Mutate only affects assign/alias but not captures.
|
||||
|
||||
`MutateConditionally` is an alternative in which the mutation _may_ happen depending on the type of the value. The conditional variant is not generally used and included for completeness.
|
||||
|
||||
|
||||
|
||||
#### MutateTransitiveConditionally (and MutateTransitive)
|
||||
|
||||
`MutateTransitiveConditionally` represents an operation that may mutate _any_ aspect of a value, including reaching arbitrarily deep into nested values to mutate them. This is the default semantic for unknown functions — we have no idea what they do, so we assume that they are idempotent but may mutate any aspect of the mutable values that are passed to them.
|
||||
|
||||
There is also `MutateTransitive` for completeness, but this is not generally used.
|
||||
|
||||
### Side Effects
|
||||
|
||||
Finally, there are a few effects that describe error, or potential error, conditions:
|
||||
|
||||
- `MutateFrozen` is always an error, because it indicates known mutation of a value that should not be mutated.
|
||||
- `MutateGlobal` indicates known mutation of a global value, which is not safe during render. This effect is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
|
||||
- `Impure` indicates calling some other logic that is impure/side-effecting. This is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
|
||||
- TODO: we could probably merge this and MutateGlobal
|
||||
- `Render` indicates a value that is not mutated, but is known to be called during render. It's used for a few particular places like JSX tags and JSX children, which we assume are accessed during render (while other props may be event handlers etc). This helps to detect more MutateGlobal/Impure effects and reject more invalid programs.
|
||||
|
||||
|
||||
## Rules
|
||||
|
||||
### Mutation of Alias Mutates the Source Value
|
||||
|
||||
```
|
||||
Alias a <- b
|
||||
Mutate a
|
||||
=>
|
||||
Mutate b
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const a = maybeIdentity(b); // Alias a <- b
|
||||
a.property = value; // a could be b, so this mutates b
|
||||
```
|
||||
|
||||
### Mutation of Assignment Mutates the Source Value
|
||||
|
||||
```
|
||||
Assign a <- b
|
||||
Mutate a
|
||||
=>
|
||||
Mutate b
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const a = b;
|
||||
a.property = value // a _is_ b, this mutates b
|
||||
```
|
||||
|
||||
### Mutation of CreateFrom Mutates the Source Value
|
||||
|
||||
```
|
||||
CreateFrom a <- b
|
||||
Mutate a
|
||||
=>
|
||||
Mutate b
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const a = b[index];
|
||||
a.property = value // the contents of b are transitively mutated
|
||||
```
|
||||
|
||||
|
||||
### Mutation of Capture Does *Not* Mutate the Source Value
|
||||
|
||||
```
|
||||
Capture a <- b
|
||||
Mutate a
|
||||
!=>
|
||||
~Mutate b~
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const a = {};
|
||||
a.b = b;
|
||||
a.property = value; // mutates a, not b
|
||||
```
|
||||
|
||||
### Mutation of Source Affects Alias, Assignment, CreateFrom, and Capture
|
||||
|
||||
```
|
||||
Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
|
||||
Mutate b
|
||||
=>
|
||||
Mutate a
|
||||
```
|
||||
|
||||
A derived value changes when it's source value is mutated.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const x = {};
|
||||
const y = [x];
|
||||
x.y = true; // this changes the value within `y` ie mutates y
|
||||
```
|
||||
|
||||
|
||||
### TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source
|
||||
|
||||
```
|
||||
Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
|
||||
MutateTransitive a
|
||||
=>
|
||||
MutateTransitive b
|
||||
```
|
||||
|
||||
Remember, the intuition for a transitive mutation is that it's something that could traverse arbitrarily deep into an object and mutate whatever it finds. Imagine something that recurses into every nested object/array and sets `.field = value`. Given a function `mutate()` that does this, then:
|
||||
|
||||
```js
|
||||
const a = b; // assign
|
||||
mutate(a); // clearly can transitively mutate b
|
||||
|
||||
const a = maybeIdentity(b); // alias
|
||||
mutate(a); // clearly can transitively mutate b
|
||||
|
||||
const a = b[index]; // createfrom
|
||||
mutate(a); // clearly can transitively mutate b
|
||||
|
||||
const a = {};
|
||||
a.b = b; // capture
|
||||
mutate(a); // can transitively mutate b
|
||||
```
|
||||
|
||||
### Freeze Does Not Freeze the Value
|
||||
|
||||
Freeze does not freeze the value itself:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
!=>
|
||||
~Freeze x~
|
||||
```
|
||||
|
||||
This means that subsequent mutations of the original value are valid:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
Mutate x
|
||||
=>
|
||||
Mutate x (mutation is ok)
|
||||
```
|
||||
|
||||
As well as mutations through other assignments/aliases/captures/createfroms of the original value:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
Alias z <- x OR Capture z <- x OR CreateFrom z <- x OR Assign z <- x
|
||||
Mutate z
|
||||
=>
|
||||
Mutate x (mutation is ok)
|
||||
```
|
||||
|
||||
### Freeze Freezes The Reference
|
||||
|
||||
Although freeze doesn't freeze the value, it does affect the reference. The reference cannot be used to mutate.
|
||||
|
||||
Conditional mutations of the reference are no-ops:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
MutateConditional y
|
||||
=>
|
||||
(no mutation)
|
||||
```
|
||||
|
||||
And known mutations of the reference are errors:
|
||||
|
||||
```
|
||||
Create x
|
||||
Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
|
||||
Freeze y
|
||||
MutateConditional y
|
||||
=>
|
||||
MutateFrozen y error=...
|
||||
```
|
||||
|
||||
### Corollary: Transitivity of Assign/Alias/CreateFrom/Capture
|
||||
|
||||
A key part of the inference model is inferring a signature for function expressions. The signature is a minimal set of effects that describes the publicly observable behavior of the function. This can include "global" effects like side effects (MutateGlobal/Impure) as well as mutations/aliasing of parameters and free variables.
|
||||
|
||||
In order to determine the aliasing of params and free variables into each other and/or the return value, we may encounter chains of assign, alias, createfrom, and capture effects. For example:
|
||||
|
||||
```js
|
||||
const f = (x) => {
|
||||
const y = [x]; // capture y <- x
|
||||
const z = y[0]; // createfrom z <- y
|
||||
return z; // assign return <- z
|
||||
}
|
||||
// <Effect> return <- x
|
||||
```
|
||||
|
||||
In this example we can see that there should be some effect on `f` that tracks the flow of data from `x` into the return value. The key constraint is preserving the semantics around how local/transitive mutations of the destination would affect the source.
|
||||
|
||||
#### Each of the effects is transitive with itself
|
||||
|
||||
```
|
||||
Assign b <- a
|
||||
Assign c <- b
|
||||
=>
|
||||
Assign c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Alias b <- a
|
||||
Alias c <- b
|
||||
=>
|
||||
Alias c <- a
|
||||
```
|
||||
|
||||
```
|
||||
CreateFrom b <- a
|
||||
CreateFrom c <- b
|
||||
=>
|
||||
CreateFrom c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Capture b <- a
|
||||
Capture c <- b
|
||||
=>
|
||||
Capture c <- a
|
||||
```
|
||||
|
||||
#### Alias > Assign
|
||||
|
||||
```
|
||||
Assign b <- a
|
||||
Alias c <- b
|
||||
=>
|
||||
Alias c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Alias b <- a
|
||||
Assign c <- b
|
||||
=>
|
||||
Alias c <- a
|
||||
```
|
||||
|
||||
### CreateFrom > Assign/Alias
|
||||
|
||||
Intuition:
|
||||
|
||||
```
|
||||
CreateFrom b <- a
|
||||
Alias c <- b OR Assign c <- b
|
||||
=>
|
||||
CreateFrom c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Alias b <- a OR Assign b <- a
|
||||
CreateFrom c <- b
|
||||
=>
|
||||
CreateFrom c <- a
|
||||
```
|
||||
|
||||
### Capture > Assign/Alias
|
||||
|
||||
Intuition: capturing means that a local mutation of the destination will not affect the source, so we preserve the capture.
|
||||
|
||||
```
|
||||
Capture b <- a
|
||||
Alias c <- b OR Assign c <- b
|
||||
=>
|
||||
Capture c <- a
|
||||
```
|
||||
|
||||
```
|
||||
Alias b <- a OR Assign b <- a
|
||||
Capture c <- b
|
||||
=>
|
||||
Capture c <- a
|
||||
```
|
||||
|
||||
### Capture And CreateFrom
|
||||
|
||||
Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations:
|
||||
|
||||
Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
|
||||
|
||||
```js
|
||||
const b = [a]; // capture
|
||||
const c = b[0]; // createfrom
|
||||
mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom
|
||||
```
|
||||
|
||||
We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias.
|
||||
|
||||
```
|
||||
Capture b <- a
|
||||
CreateFrom c <- b
|
||||
=>
|
||||
Alias c <- a
|
||||
```
|
||||
|
||||
Meanwhile the opposite direction preserves the capture, because the result is not the same as the source:
|
||||
|
||||
```js
|
||||
const b = a[0]; // createfrom
|
||||
const c = [b]; // capture
|
||||
mutate(c); // does not mutate a, so the result must be Capture
|
||||
```
|
||||
|
||||
```
|
||||
CreateFrom b <- a
|
||||
Capture c <- b
|
||||
=>
|
||||
Capture c <- a
|
||||
```
|
||||
@@ -38,13 +38,15 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
19 | useEffect(() => setState(2), []);
|
||||
17 | * $2 = Function context=setState
|
||||
18 | */
|
||||
> 19 | useEffect(() => setState(2), []);
|
||||
| ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `setState` is accessed before it is declared (19:19)
|
||||
|
||||
InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `setState` is accessed before it is declared (21:21)
|
||||
20 |
|
||||
> 21 | const [state, setState] = useState(0);
|
||||
| ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `setState` to before it is first referenced (21:21)
|
||||
21 | const [state, setState] = useState(0);
|
||||
22 | return <Stringify state={state} />;
|
||||
23 | }
|
||||
24 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, mutate} from 'shared-runtime';
|
||||
|
||||
function Component({foo, bar}) {
|
||||
let x = {foo};
|
||||
let y = {bar};
|
||||
const f0 = function () {
|
||||
let a = {y};
|
||||
let b = {x};
|
||||
a.y.x = b;
|
||||
};
|
||||
f0();
|
||||
mutate(y);
|
||||
return <Stringify x={y} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 2, bar: 3}],
|
||||
sequentialRenders: [
|
||||
{foo: 2, bar: 3},
|
||||
{foo: 2, bar: 3},
|
||||
{foo: 2, bar: 4},
|
||||
{foo: 3, bar: 4},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, mutate } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(3);
|
||||
const { foo, bar } = t0;
|
||||
let t1;
|
||||
if ($[0] !== bar || $[1] !== foo) {
|
||||
const x = { foo };
|
||||
const y = { bar };
|
||||
const f0 = function () {
|
||||
const a = { y };
|
||||
const b = { x };
|
||||
a.y.x = b;
|
||||
};
|
||||
|
||||
f0();
|
||||
mutate(y);
|
||||
t1 = <Stringify x={y} />;
|
||||
$[0] = bar;
|
||||
$[1] = foo;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ foo: 2, bar: 3 }],
|
||||
sequentialRenders: [
|
||||
{ foo: 2, bar: 3 },
|
||||
{ foo: 2, bar: 3 },
|
||||
{ foo: 2, bar: 4 },
|
||||
{ foo: 3, bar: 4 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
|
||||
<div>{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
|
||||
<div>{"x":{"bar":4,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
|
||||
<div>{"x":{"bar":4,"x":{"x":{"foo":3}},"wat0":"joe"}}</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import {Stringify, mutate} from 'shared-runtime';
|
||||
|
||||
function Component({foo, bar}) {
|
||||
let x = {foo};
|
||||
let y = {bar};
|
||||
const f0 = function () {
|
||||
let a = {y};
|
||||
let b = {x};
|
||||
a.y.x = b;
|
||||
};
|
||||
f0();
|
||||
mutate(y);
|
||||
return <Stringify x={y} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 2, bar: 3}],
|
||||
sequentialRenders: [
|
||||
{foo: 2, bar: 3},
|
||||
{foo: 2, bar: 3},
|
||||
{foo: 2, bar: 4},
|
||||
{foo: 3, bar: 4},
|
||||
],
|
||||
};
|
||||
@@ -31,13 +31,15 @@ function Component({content, refetch}) {
|
||||
## Error
|
||||
|
||||
```
|
||||
17 | // This has to error: onRefetch needs to memoize with `content` as a
|
||||
18 | // dependency, but the dependency comes later
|
||||
> 19 | const {data = null} = content;
|
||||
| ^^^^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `data` to before it is first referenced (19:19)
|
||||
20 |
|
||||
21 | return <Foo data={data} onSubmit={onSubmit} />;
|
||||
22 | }
|
||||
9 | // TDZ violation!
|
||||
10 | const onRefetch = useCallback(() => {
|
||||
> 11 | refetch(data);
|
||||
| ^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `data` is accessed before it is declared (11:11)
|
||||
|
||||
InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `data` is accessed before it is declared (19:19)
|
||||
12 | }, [refetch]);
|
||||
13 |
|
||||
14 | // The context variable gets frozen here since it's passed to a hook
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => ({a}), [a, b]);
|
||||
const f = () => {
|
||||
return identity(x);
|
||||
};
|
||||
const x2 = f();
|
||||
x2.b = b;
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import { identity, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(10);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
let x;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
t1 = { a };
|
||||
x = t1;
|
||||
const f = () => identity(x);
|
||||
|
||||
const x2 = f();
|
||||
x2.b = b;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = x;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
x = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== a || $[5] !== b) {
|
||||
t2 = [a, b];
|
||||
$[4] = a;
|
||||
$[5] = b;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== t2 || $[8] !== x) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={x} />;
|
||||
$[7] = t2;
|
||||
$[8] = x;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
|
||||
<div>{"inputs":[0,1],"output":{"a":0,"b":1}}</div>
|
||||
<div>{"inputs":[1,1],"output":{"a":1,"b":1}}</div>
|
||||
<div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => ({a}), [a, b]);
|
||||
const f = () => {
|
||||
return identity(x);
|
||||
};
|
||||
const x2 = f();
|
||||
x2.b = b;
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => ({a}), [a, b]);
|
||||
const x2 = identity(x);
|
||||
x2.b = b;
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import { identity, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(10);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
let x;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
t1 = { a };
|
||||
x = t1;
|
||||
const x2 = identity(x);
|
||||
x2.b = b;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = x;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
x = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== a || $[5] !== b) {
|
||||
t2 = [a, b];
|
||||
$[4] = a;
|
||||
$[5] = b;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== t2 || $[8] !== x) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={x} />;
|
||||
$[7] = t2;
|
||||
$[8] = x;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
|
||||
<div>{"inputs":[0,1],"output":{"a":0,"b":1}}</div>
|
||||
<div>{"inputs":[1,1],"output":{"a":1,"b":1}}</div>
|
||||
<div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => ({a}), [a, b]);
|
||||
const x2 = identity(x);
|
||||
x2.b = b;
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableNewMutationAliasingModel
|
||||
|
||||
import fbt from 'fbt';
|
||||
|
||||
component Component() {
|
||||
const sections = Object.keys(items);
|
||||
|
||||
for (let i = 0; i < sections.length; i += 3) {
|
||||
chunks.push(
|
||||
sections.slice(i, i + 3).map(section => {
|
||||
return <Child />;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return <Child />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
|
||||
import fbt from "fbt";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const sections = Object.keys(items);
|
||||
for (let i = 0; i < sections.length; i = i + 3, i) {
|
||||
chunks.push(sections.slice(i, i + 3).map(_temp));
|
||||
}
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Child />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp(section) {
|
||||
return <Child />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,17 @@
|
||||
// @flow @enableNewMutationAliasingModel
|
||||
|
||||
import fbt from 'fbt';
|
||||
|
||||
component Component() {
|
||||
const sections = Object.keys(items);
|
||||
|
||||
for (let i = 0; i < sections.length; i += 3) {
|
||||
chunks.push(
|
||||
sections.slice(i, i + 3).map(section => {
|
||||
return <Child />;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return <Child />;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
const x = {};
|
||||
const fn = () => {
|
||||
new Object()
|
||||
.build(x)
|
||||
.build({})
|
||||
.build({})
|
||||
.build({})
|
||||
.build({})
|
||||
.build({})
|
||||
.build({});
|
||||
};
|
||||
return <Stringify x={x} fn={fn} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const x = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const fn = () => {
|
||||
new Object()
|
||||
.build(x)
|
||||
.build({})
|
||||
.build({})
|
||||
.build({})
|
||||
.build({})
|
||||
.build({})
|
||||
.build({});
|
||||
};
|
||||
|
||||
t1 = <Stringify x={x} fn={fn} />;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,14 @@
|
||||
function Component() {
|
||||
const x = {};
|
||||
const fn = () => {
|
||||
new Object()
|
||||
.build(x)
|
||||
.build({})
|
||||
.build({})
|
||||
.build({})
|
||||
.build({})
|
||||
.build({})
|
||||
.build({});
|
||||
};
|
||||
return <Stringify x={x} fn={fn} />;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component({a, b}) {
|
||||
const y = {a};
|
||||
const x = {b};
|
||||
const f = () => {
|
||||
let z = null;
|
||||
while (z == null) {
|
||||
z = x;
|
||||
}
|
||||
// z is a phi with a backedge, and we don't realize it could be x,
|
||||
// and therefore fail to record a Capture x <- y effect for this
|
||||
// function expression
|
||||
z.y = y;
|
||||
};
|
||||
f();
|
||||
mutate(x);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(t0) {
|
||||
const $ = _c(3);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const y = { a };
|
||||
const x = { b };
|
||||
const f = () => {
|
||||
let z = null;
|
||||
while (z == null) {
|
||||
z = x;
|
||||
}
|
||||
|
||||
z.y = y;
|
||||
};
|
||||
|
||||
f();
|
||||
mutate(x);
|
||||
t1 = <div>{x}</div>;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,17 @@
|
||||
function Component({a, b}) {
|
||||
const y = {a};
|
||||
const x = {b};
|
||||
const f = () => {
|
||||
let z = null;
|
||||
while (z == null) {
|
||||
z = x;
|
||||
}
|
||||
// z is a phi with a backedge, and we don't realize it could be x,
|
||||
// and therefore fail to record a Capture x <- y effect for this
|
||||
// function expression
|
||||
z.y = y;
|
||||
};
|
||||
f();
|
||||
mutate(x);
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableNewMutationAliasingModel
|
||||
|
||||
import {identity, Stringify, useFragment} from 'shared-runtime';
|
||||
|
||||
component Example() {
|
||||
const data = useFragment();
|
||||
|
||||
const {a, b} = identity(data);
|
||||
|
||||
const el = <Stringify tooltip={b} />;
|
||||
|
||||
identity(a.at(0));
|
||||
|
||||
return <Stringify icon={el} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
|
||||
import { identity, Stringify, useFragment } from "shared-runtime";
|
||||
|
||||
function Example() {
|
||||
const $ = _c(2);
|
||||
const data = useFragment();
|
||||
let t0;
|
||||
if ($[0] !== data) {
|
||||
const { a, b } = identity(data);
|
||||
|
||||
const el = <Stringify tooltip={b} />;
|
||||
|
||||
identity(a.at(0));
|
||||
|
||||
t0 = <Stringify icon={el} />;
|
||||
$[0] = data;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,15 @@
|
||||
// @flow @enableNewMutationAliasingModel
|
||||
|
||||
import {identity, Stringify, useFragment} from 'shared-runtime';
|
||||
|
||||
component Example() {
|
||||
const data = useFragment();
|
||||
|
||||
const {a, b} = identity(data);
|
||||
|
||||
const el = <Stringify tooltip={b} />;
|
||||
|
||||
identity(a.at(0));
|
||||
|
||||
return <Stringify icon={el} />;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:true
|
||||
|
||||
export const App = () => {
|
||||
const [selected, setSelected] = useState(new Set<string>());
|
||||
const onSelectedChange = (value: string) => {
|
||||
const newSelected = new Set(selected);
|
||||
if (newSelected.has(value)) {
|
||||
// This should not count as a mutation of `selected`
|
||||
newSelected.delete(value);
|
||||
} else {
|
||||
// This should not count as a mutation of `selected`
|
||||
newSelected.add(value);
|
||||
}
|
||||
setSelected(newSelected);
|
||||
};
|
||||
|
||||
return <Stringify selected={selected} onSelectedChange={onSelectedChange} />;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:true
|
||||
|
||||
export const App = () => {
|
||||
const $ = _c(6);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = new Set();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const [selected, setSelected] = useState(t0);
|
||||
let t1;
|
||||
if ($[1] !== selected) {
|
||||
t1 = (value) => {
|
||||
const newSelected = new Set(selected);
|
||||
if (newSelected.has(value)) {
|
||||
newSelected.delete(value);
|
||||
} else {
|
||||
newSelected.add(value);
|
||||
}
|
||||
|
||||
setSelected(newSelected);
|
||||
};
|
||||
$[1] = selected;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const onSelectedChange = t1;
|
||||
let t2;
|
||||
if ($[3] !== onSelectedChange || $[4] !== selected) {
|
||||
t2 = <Stringify selected={selected} onSelectedChange={onSelectedChange} />;
|
||||
$[3] = onSelectedChange;
|
||||
$[4] = selected;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
return t2;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,18 @@
|
||||
// @enableNewMutationAliasingModel:true
|
||||
|
||||
export const App = () => {
|
||||
const [selected, setSelected] = useState(new Set<string>());
|
||||
const onSelectedChange = (value: string) => {
|
||||
const newSelected = new Set(selected);
|
||||
if (newSelected.has(value)) {
|
||||
// This should not count as a mutation of `selected`
|
||||
newSelected.delete(value);
|
||||
} else {
|
||||
// This should not count as a mutation of `selected`
|
||||
newSelected.add(value);
|
||||
}
|
||||
setSelected(newSelected);
|
||||
};
|
||||
|
||||
return <Stringify selected={selected} onSelectedChange={onSelectedChange} />;
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
mutate,
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b, c}: {a: number; b: number; c: number}) {
|
||||
const x = useMemo(() => [{value: a}], [a, b, c]);
|
||||
if (b === 0) {
|
||||
// This object should only depend on c, it cannot be affected by the later mutation
|
||||
x.push({value: c});
|
||||
} else {
|
||||
// This mutation shouldn't affect the object in the consequent
|
||||
mutate(x);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[a, b, c]} output={x} />;
|
||||
{/* TODO: should only depend on c */}
|
||||
<ValidateMemoization inputs={[a, b, c]} output={x[0]} />;
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0, c: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0, c: 0},
|
||||
{a: 0, b: 1, c: 0},
|
||||
{a: 1, b: 1, c: 0},
|
||||
{a: 1, b: 1, c: 1},
|
||||
{a: 1, b: 1, c: 0},
|
||||
{a: 1, b: 0, c: 0},
|
||||
{a: 0, b: 0, c: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
mutate,
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(22);
|
||||
const { a, b, c } = t0;
|
||||
let t1;
|
||||
let x;
|
||||
if ($[0] !== a || $[1] !== b || $[2] !== c) {
|
||||
t1 = [{ value: a }];
|
||||
x = t1;
|
||||
if (b === 0) {
|
||||
x.push({ value: c });
|
||||
} else {
|
||||
mutate(x);
|
||||
}
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = c;
|
||||
$[3] = x;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
x = $[3];
|
||||
t1 = $[4];
|
||||
}
|
||||
let t2;
|
||||
if ($[5] !== a || $[6] !== b || $[7] !== c) {
|
||||
t2 = [a, b, c];
|
||||
$[5] = a;
|
||||
$[6] = b;
|
||||
$[7] = c;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
let t3;
|
||||
if ($[9] !== t2 || $[10] !== x) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={x} />;
|
||||
$[9] = t2;
|
||||
$[10] = x;
|
||||
$[11] = t3;
|
||||
} else {
|
||||
t3 = $[11];
|
||||
}
|
||||
let t4;
|
||||
if ($[12] !== a || $[13] !== b || $[14] !== c) {
|
||||
t4 = [a, b, c];
|
||||
$[12] = a;
|
||||
$[13] = b;
|
||||
$[14] = c;
|
||||
$[15] = t4;
|
||||
} else {
|
||||
t4 = $[15];
|
||||
}
|
||||
let t5;
|
||||
if ($[16] !== t4 || $[17] !== x[0]) {
|
||||
t5 = <ValidateMemoization inputs={t4} output={x[0]} />;
|
||||
$[16] = t4;
|
||||
$[17] = x[0];
|
||||
$[18] = t5;
|
||||
} else {
|
||||
t5 = $[18];
|
||||
}
|
||||
let t6;
|
||||
if ($[19] !== t3 || $[20] !== t5) {
|
||||
t6 = (
|
||||
<>
|
||||
{t3};{t5};
|
||||
</>
|
||||
);
|
||||
$[19] = t3;
|
||||
$[20] = t5;
|
||||
$[21] = t6;
|
||||
} else {
|
||||
t6 = $[21];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0, c: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0, c: 0 },
|
||||
{ a: 0, b: 1, c: 0 },
|
||||
{ a: 1, b: 1, c: 0 },
|
||||
{ a: 1, b: 1, c: 1 },
|
||||
{ a: 1, b: 1, c: 0 },
|
||||
{ a: 1, b: 0, c: 0 },
|
||||
{ a: 0, b: 0, c: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}</div>;<div>{"inputs":[0,0,0],"output":{"value":0}}</div>;
|
||||
<div>{"inputs":[0,1,0],"output":[{"value":0},"joe"]}</div>;<div>{"inputs":[0,1,0],"output":{"value":0}}</div>;
|
||||
<div>{"inputs":[1,1,0],"output":[{"value":1},"joe"]}</div>;<div>{"inputs":[1,1,0],"output":{"value":1}}</div>;
|
||||
<div>{"inputs":[1,1,1],"output":[{"value":1},"joe"]}</div>;<div>{"inputs":[1,1,1],"output":{"value":1}}</div>;
|
||||
<div>{"inputs":[1,1,0],"output":[{"value":1},"joe"]}</div>;<div>{"inputs":[1,1,0],"output":{"value":1}}</div>;
|
||||
<div>{"inputs":[1,0,0],"output":[{"value":1},{"value":0}]}</div>;<div>{"inputs":[1,0,0],"output":{"value":1}}</div>;
|
||||
<div>{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}</div>;<div>{"inputs":[0,0,0],"output":{"value":0}}</div>;
|
||||
@@ -0,0 +1,41 @@
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
mutate,
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b, c}: {a: number; b: number; c: number}) {
|
||||
const x = useMemo(() => [{value: a}], [a, b, c]);
|
||||
if (b === 0) {
|
||||
// This object should only depend on c, it cannot be affected by the later mutation
|
||||
x.push({value: c});
|
||||
} else {
|
||||
// This mutation shouldn't affect the object in the consequent
|
||||
mutate(x);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[a, b, c]} output={x} />;
|
||||
{/* TODO: should only depend on c */}
|
||||
<ValidateMemoization inputs={[a, b, c]} output={x[0]} />;
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0, c: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0, c: 0},
|
||||
{a: 0, b: 1, c: 0},
|
||||
{a: 1, b: 1, c: 0},
|
||||
{a: 1, b: 1, c: 1},
|
||||
{a: 1, b: 1, c: 0},
|
||||
{a: 1, b: 0, c: 0},
|
||||
{a: 0, b: 0, c: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => [{a}], [a]);
|
||||
const f = () => {
|
||||
const y = typedCreateFrom(x);
|
||||
const z = typedCapture(y);
|
||||
return z;
|
||||
};
|
||||
const z = f();
|
||||
// does not mutate x, so x should not depend on b
|
||||
typedMutate(z, b);
|
||||
|
||||
// TODO: this *should* only depend on `a`
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(10);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
let x;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
t1 = [{ a }];
|
||||
x = t1;
|
||||
const f = () => {
|
||||
const y = typedCreateFrom(x);
|
||||
const z = typedCapture(y);
|
||||
return z;
|
||||
};
|
||||
|
||||
const z_0 = f();
|
||||
|
||||
typedMutate(z_0, b);
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = x;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
x = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== a || $[5] !== b) {
|
||||
t2 = [a, b];
|
||||
$[4] = a;
|
||||
$[5] = b;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== t2 || $[8] !== x) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={x} />;
|
||||
$[7] = t2;
|
||||
$[8] = x;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0,0],"output":[{"a":0}]}</div>
|
||||
<div>{"inputs":[0,1],"output":[{"a":0}]}</div>
|
||||
<div>{"inputs":[1,1],"output":[{"a":1}]}</div>
|
||||
<div>{"inputs":[0,0],"output":[{"a":0}]}</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => [{a}], [a]);
|
||||
const f = () => {
|
||||
const y = typedCreateFrom(x);
|
||||
const z = typedCapture(y);
|
||||
return z;
|
||||
};
|
||||
const z = f();
|
||||
// does not mutate x, so x should not depend on b
|
||||
typedMutate(z, b);
|
||||
|
||||
// TODO: this *should* only depend on `a`
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const o: any = useMemo(() => ({a}), [a]);
|
||||
const x: Array<any> = useMemo(() => [o], [o, b]);
|
||||
const y = typedCapture(x);
|
||||
const z = typedCapture(y);
|
||||
x.push(z);
|
||||
x.push(b);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[a]} output={o} />;
|
||||
<ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(20);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== a) {
|
||||
t2 = { a };
|
||||
$[0] = a;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
t1 = t2;
|
||||
const o = t1;
|
||||
let t3;
|
||||
let x;
|
||||
if ($[2] !== b || $[3] !== o) {
|
||||
t3 = [o];
|
||||
x = t3;
|
||||
const y = typedCapture(x);
|
||||
const z = typedCapture(y);
|
||||
x.push(z);
|
||||
x.push(b);
|
||||
$[2] = b;
|
||||
$[3] = o;
|
||||
$[4] = x;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
x = $[4];
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== a) {
|
||||
t4 = [a];
|
||||
$[6] = a;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== o || $[9] !== t4) {
|
||||
t5 = <ValidateMemoization inputs={t4} output={o} />;
|
||||
$[8] = o;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
let t6;
|
||||
if ($[11] !== a || $[12] !== b) {
|
||||
t6 = [a, b];
|
||||
$[11] = a;
|
||||
$[12] = b;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
let t7;
|
||||
if ($[14] !== t6 || $[15] !== x) {
|
||||
t7 = <ValidateMemoization inputs={t6} output={x} />;
|
||||
$[14] = t6;
|
||||
$[15] = x;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
let t8;
|
||||
if ($[17] !== t5 || $[18] !== t7) {
|
||||
t8 = (
|
||||
<>
|
||||
{t5};{t7};
|
||||
</>
|
||||
);
|
||||
$[17] = t5;
|
||||
$[18] = t7;
|
||||
$[19] = t8;
|
||||
} else {
|
||||
t8 = $[19];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0],"output":{"a":0}}</div>;<div>{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}</div>;
|
||||
<div>{"inputs":[0],"output":{"a":0}}</div>;<div>{"inputs":[0,1],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],1]}</div>;
|
||||
<div>{"inputs":[1],"output":{"a":1}}</div>;<div>{"inputs":[1,1],"output":[{"a":1},[["[[ cyclic ref *2 ]]"]],1]}</div>;
|
||||
<div>{"inputs":[0],"output":{"a":0}}</div>;<div>{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}</div>;
|
||||
@@ -0,0 +1,34 @@
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const o: any = useMemo(() => ({a}), [a]);
|
||||
const x: Array<any> = useMemo(() => [o], [o, b]);
|
||||
const y = typedCapture(x);
|
||||
const z = typedCapture(y);
|
||||
x.push(z);
|
||||
x.push(b);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[a]} output={o} />;
|
||||
<ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}: {a: number; b: number}) {
|
||||
const x = useMemo(() => ({a}), [a, b]);
|
||||
const f = () => {
|
||||
const y = typedCapture(x);
|
||||
const z = typedCreateFrom(y);
|
||||
return z;
|
||||
};
|
||||
const z = f();
|
||||
// mutates x
|
||||
typedMutate(z, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(10);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
let x;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
t1 = { a };
|
||||
x = t1;
|
||||
const f = () => {
|
||||
const y = typedCapture(x);
|
||||
const z = typedCreateFrom(y);
|
||||
return z;
|
||||
};
|
||||
|
||||
const z_0 = f();
|
||||
|
||||
typedMutate(z_0, b);
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = x;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
x = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== a || $[5] !== b) {
|
||||
t2 = [a, b];
|
||||
$[4] = a;
|
||||
$[5] = b;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== t2 || $[8] !== x) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={x} />;
|
||||
$[7] = t2;
|
||||
$[8] = x;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"property":0}}</div>
|
||||
<div>{"inputs":[0,1],"output":{"a":0,"property":1}}</div>
|
||||
<div>{"inputs":[1,1],"output":{"a":1,"property":1}}</div>
|
||||
<div>{"inputs":[0,0],"output":{"a":0,"property":0}}</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}: {a: number; b: number}) {
|
||||
const x = useMemo(() => ({a}), [a, b]);
|
||||
const f = () => {
|
||||
const y = typedCapture(x);
|
||||
const z = typedCreateFrom(y);
|
||||
return z;
|
||||
};
|
||||
const z = f();
|
||||
// mutates x
|
||||
typedMutate(z, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}: {a: number; b: number}) {
|
||||
const x = useMemo(() => ({a}), [a, b]);
|
||||
const y = typedCapture(x);
|
||||
const z = typedCreateFrom(y);
|
||||
// mutates x
|
||||
typedMutate(z, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(10);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
let x;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
t1 = { a };
|
||||
x = t1;
|
||||
const y = typedCapture(x);
|
||||
const z = typedCreateFrom(y);
|
||||
|
||||
typedMutate(z, b);
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = x;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
x = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== a || $[5] !== b) {
|
||||
t2 = [a, b];
|
||||
$[4] = a;
|
||||
$[5] = b;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== t2 || $[8] !== x) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={x} />;
|
||||
$[7] = t2;
|
||||
$[8] = x;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"property":0}}</div>
|
||||
<div>{"inputs":[0,1],"output":{"a":0,"property":1}}</div>
|
||||
<div>{"inputs":[1,1],"output":{"a":1,"property":1}}</div>
|
||||
<div>{"inputs":[0,0],"output":{"a":0,"property":0}}</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}: {a: number; b: number}) {
|
||||
const x = useMemo(() => ({a}), [a, b]);
|
||||
const y = typedCapture(x);
|
||||
const z = typedCreateFrom(y);
|
||||
// mutates x
|
||||
typedMutate(z, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => [{a}], [a]);
|
||||
const y = typedCreateFrom(x);
|
||||
const z = typedCapture(y);
|
||||
// does not mutate x, so x should not depend on b
|
||||
typedMutate(z, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== a) {
|
||||
t2 = [{ a }];
|
||||
$[0] = a;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
t1 = t2;
|
||||
const x = t1;
|
||||
const y = typedCreateFrom(x);
|
||||
const z = typedCapture(y);
|
||||
|
||||
typedMutate(z, b);
|
||||
let t3;
|
||||
if ($[2] !== a) {
|
||||
t3 = [a];
|
||||
$[2] = a;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== t3 || $[5] !== x) {
|
||||
t4 = <ValidateMemoization inputs={t3} output={x} />;
|
||||
$[4] = t3;
|
||||
$[5] = x;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0],"output":[{"a":0}]}</div>
|
||||
<div>{"inputs":[0],"output":[{"a":0}]}</div>
|
||||
<div>{"inputs":[1],"output":[{"a":1}]}</div>
|
||||
<div>{"inputs":[0],"output":[{"a":0}]}</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => [{a}], [a]);
|
||||
const y = typedCreateFrom(x);
|
||||
const z = typedCapture(y);
|
||||
// does not mutate x, so x should not depend on b
|
||||
typedMutate(z, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => [{a}], [a, b]);
|
||||
let z: any;
|
||||
if (b) {
|
||||
z = x;
|
||||
} else {
|
||||
z = typedCapture(x);
|
||||
}
|
||||
// could mutate x
|
||||
typedMutate(z, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(12);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== a) {
|
||||
t2 = { a };
|
||||
$[0] = a;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let x;
|
||||
if ($[2] !== b || $[3] !== t2) {
|
||||
t1 = [t2];
|
||||
x = t1;
|
||||
let z;
|
||||
if (b) {
|
||||
z = x;
|
||||
} else {
|
||||
z = typedCapture(x);
|
||||
}
|
||||
|
||||
typedMutate(z, b);
|
||||
$[2] = b;
|
||||
$[3] = t2;
|
||||
$[4] = x;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
x = $[4];
|
||||
t1 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== a || $[7] !== b) {
|
||||
t3 = [a, b];
|
||||
$[6] = a;
|
||||
$[7] = b;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
let t4;
|
||||
if ($[9] !== t3 || $[10] !== x) {
|
||||
t4 = <ValidateMemoization inputs={t3} output={x} />;
|
||||
$[9] = t3;
|
||||
$[10] = x;
|
||||
$[11] = t4;
|
||||
} else {
|
||||
t4 = $[11];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0,0],"output":[{"a":0}]}</div>
|
||||
<div>{"inputs":[0,1],"output":[{"a":0}]}</div>
|
||||
<div>{"inputs":[1,1],"output":[{"a":1}]}</div>
|
||||
<div>{"inputs":[0,0],"output":[{"a":0}]}</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
typedCapture,
|
||||
typedCreateFrom,
|
||||
typedMutate,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
const x = useMemo(() => [{a}], [a, b]);
|
||||
let z: any;
|
||||
if (b) {
|
||||
z = x;
|
||||
} else {
|
||||
z = typedCapture(x);
|
||||
}
|
||||
// could mutate x
|
||||
typedMutate(z, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 0, b: 1},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
identity,
|
||||
makeObject_Primitives,
|
||||
typedIdentity,
|
||||
useIdentity,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
// create a mutable value with input `a`
|
||||
const x = useMemo(() => makeObject_Primitives(a), [a]);
|
||||
|
||||
// freeze the value
|
||||
useIdentity(x);
|
||||
|
||||
// known to pass-through via aliasing signature
|
||||
const x2 = typedIdentity(x);
|
||||
|
||||
// Unknown function so we assume it conditionally mutates,
|
||||
// but x2 is frozen so this downgrades to a read.
|
||||
// x should *not* take b as a dependency
|
||||
identity(x2, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 1, b: 0},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
identity,
|
||||
makeObject_Primitives,
|
||||
typedIdentity,
|
||||
useIdentity,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== a) {
|
||||
t2 = makeObject_Primitives(a);
|
||||
$[0] = a;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
t1 = t2;
|
||||
const x = t1;
|
||||
|
||||
useIdentity(x);
|
||||
|
||||
const x2 = typedIdentity(x);
|
||||
|
||||
identity(x2, b);
|
||||
let t3;
|
||||
if ($[2] !== a) {
|
||||
t3 = [a];
|
||||
$[2] = a;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== t3 || $[5] !== x) {
|
||||
t4 = <ValidateMemoization inputs={t3} output={x} />;
|
||||
$[4] = t3;
|
||||
$[5] = x;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 1, b: 0 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {
|
||||
identity,
|
||||
makeObject_Primitives,
|
||||
typedIdentity,
|
||||
useIdentity,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
// create a mutable value with input `a`
|
||||
const x = useMemo(() => makeObject_Primitives(a), [a]);
|
||||
|
||||
// freeze the value
|
||||
useIdentity(x);
|
||||
|
||||
// known to pass-through via aliasing signature
|
||||
const x2 = typedIdentity(x);
|
||||
|
||||
// Unknown function so we assume it conditionally mutates,
|
||||
// but x2 is frozen so this downgrades to a read.
|
||||
// x should *not* take b as a dependency
|
||||
identity(x2, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 1, b: 0},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
|
||||
import {
|
||||
identity,
|
||||
makeObject_Primitives,
|
||||
typedIdentity,
|
||||
useIdentity,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
// create a mutable value with input `a`
|
||||
const x = makeObject_Primitives(a);
|
||||
|
||||
// known to pass-through via aliasing signature
|
||||
const x2 = typedIdentity(x);
|
||||
|
||||
// Unknown function so we assume it conditionally mutates,
|
||||
// and x is still mutable so
|
||||
identity(x2, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 1, b: 0},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
|
||||
import {
|
||||
identity,
|
||||
makeObject_Primitives,
|
||||
typedIdentity,
|
||||
useIdentity,
|
||||
ValidateMemoization,
|
||||
} from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(9);
|
||||
const { a, b } = t0;
|
||||
let x;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
x = makeObject_Primitives(a);
|
||||
|
||||
const x2 = typedIdentity(x);
|
||||
|
||||
identity(x2, b);
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = x;
|
||||
} else {
|
||||
x = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== a || $[4] !== b) {
|
||||
t1 = [a, b];
|
||||
$[3] = a;
|
||||
$[4] = b;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
let t2;
|
||||
if ($[6] !== t1 || $[7] !== x) {
|
||||
t2 = <ValidateMemoization inputs={t1} output={x} />;
|
||||
$[6] = t1;
|
||||
$[7] = x;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: 0 }],
|
||||
sequentialRenders: [
|
||||
{ a: 0, b: 0 },
|
||||
{ a: 1, b: 0 },
|
||||
{ a: 1, b: 1 },
|
||||
{ a: 0, b: 1 },
|
||||
{ a: 0, b: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[1,0],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[1,1],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[0,1],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
<div>{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
// @enableNewMutationAliasingModel
|
||||
|
||||
import {
|
||||
identity,
|
||||
makeObject_Primitives,
|
||||
typedIdentity,
|
||||
useIdentity,
|
||||
ValidateMemoization,
|
||||
} from 'shared-runtime';
|
||||
|
||||
function Component({a, b}) {
|
||||
// create a mutable value with input `a`
|
||||
const x = makeObject_Primitives(a);
|
||||
|
||||
// known to pass-through via aliasing signature
|
||||
const x2 = typedIdentity(x);
|
||||
|
||||
// Unknown function so we assume it conditionally mutates,
|
||||
// and x is still mutable so
|
||||
identity(x2, b);
|
||||
|
||||
return <ValidateMemoization inputs={[a, b]} output={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 0}],
|
||||
sequentialRenders: [
|
||||
{a: 0, b: 0},
|
||||
{a: 1, b: 0},
|
||||
{a: 1, b: 1},
|
||||
{a: 0, b: 1},
|
||||
{a: 0, b: 0},
|
||||
],
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({a, b, c}) {
|
||||
@@ -13,9 +14,21 @@ function Component({a, b, c}) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[a, c]} output={map} />
|
||||
<ValidateMemoization inputs={[a, c]} output={mapAlias} />
|
||||
<ValidateMemoization inputs={[b]} output={[hasB]} />
|
||||
<ValidateMemoization
|
||||
inputs={[a, c]}
|
||||
output={map}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
<ValidateMemoization
|
||||
inputs={[a, c]}
|
||||
output={mapAlias}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
<ValidateMemoization
|
||||
inputs={[b]}
|
||||
output={[hasB]}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
@@ -76,7 +90,9 @@ function Component(t0) {
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== map || $[8] !== t1) {
|
||||
t2 = <ValidateMemoization inputs={t1} output={map} />;
|
||||
t2 = (
|
||||
<ValidateMemoization inputs={t1} output={map} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[7] = map;
|
||||
$[8] = t1;
|
||||
$[9] = t2;
|
||||
@@ -94,7 +110,13 @@ function Component(t0) {
|
||||
}
|
||||
let t4;
|
||||
if ($[13] !== mapAlias || $[14] !== t3) {
|
||||
t4 = <ValidateMemoization inputs={t3} output={mapAlias} />;
|
||||
t4 = (
|
||||
<ValidateMemoization
|
||||
inputs={t3}
|
||||
output={mapAlias}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
);
|
||||
$[13] = mapAlias;
|
||||
$[14] = t3;
|
||||
$[15] = t4;
|
||||
@@ -119,7 +141,9 @@ function Component(t0) {
|
||||
}
|
||||
let t7;
|
||||
if ($[20] !== t5 || $[21] !== t6) {
|
||||
t7 = <ValidateMemoization inputs={t5} output={t6} />;
|
||||
t7 = (
|
||||
<ValidateMemoization inputs={t5} output={t6} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[20] = t5;
|
||||
$[21] = t6;
|
||||
$[22] = t7;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({a, b, c}) {
|
||||
@@ -9,9 +10,21 @@ function Component({a, b, c}) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[a, c]} output={map} />
|
||||
<ValidateMemoization inputs={[a, c]} output={mapAlias} />
|
||||
<ValidateMemoization inputs={[b]} output={[hasB]} />
|
||||
<ValidateMemoization
|
||||
inputs={[a, c]}
|
||||
output={map}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
<ValidateMemoization
|
||||
inputs={[a, c]}
|
||||
output={mapAlias}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
<ValidateMemoization
|
||||
inputs={[b]}
|
||||
output={[hasB]}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({a, b, c}) {
|
||||
@@ -13,9 +14,21 @@ function Component({a, b, c}) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[a, c]} output={set} />
|
||||
<ValidateMemoization inputs={[a, c]} output={setAlias} />
|
||||
<ValidateMemoization inputs={[b]} output={[hasB]} />
|
||||
<ValidateMemoization
|
||||
inputs={[a, c]}
|
||||
output={set}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
<ValidateMemoization
|
||||
inputs={[a, c]}
|
||||
output={setAlias}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
<ValidateMemoization
|
||||
inputs={[b]}
|
||||
output={[hasB]}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useMemo } from "react";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
@@ -76,7 +90,9 @@ function Component(t0) {
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== set || $[8] !== t1) {
|
||||
t2 = <ValidateMemoization inputs={t1} output={set} />;
|
||||
t2 = (
|
||||
<ValidateMemoization inputs={t1} output={set} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[7] = set;
|
||||
$[8] = t1;
|
||||
$[9] = t2;
|
||||
@@ -94,7 +110,13 @@ function Component(t0) {
|
||||
}
|
||||
let t4;
|
||||
if ($[13] !== setAlias || $[14] !== t3) {
|
||||
t4 = <ValidateMemoization inputs={t3} output={setAlias} />;
|
||||
t4 = (
|
||||
<ValidateMemoization
|
||||
inputs={t3}
|
||||
output={setAlias}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
);
|
||||
$[13] = setAlias;
|
||||
$[14] = t3;
|
||||
$[15] = t4;
|
||||
@@ -119,7 +141,9 @@ function Component(t0) {
|
||||
}
|
||||
let t7;
|
||||
if ($[20] !== t5 || $[21] !== t6) {
|
||||
t7 = <ValidateMemoization inputs={t5} output={t6} />;
|
||||
t7 = (
|
||||
<ValidateMemoization inputs={t5} output={t6} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[20] = t5;
|
||||
$[21] = t6;
|
||||
$[22] = t7;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({a, b, c}) {
|
||||
@@ -9,9 +10,21 @@ function Component({a, b, c}) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[a, c]} output={set} />
|
||||
<ValidateMemoization inputs={[a, c]} output={setAlias} />
|
||||
<ValidateMemoization inputs={[b]} output={[hasB]} />
|
||||
<ValidateMemoization
|
||||
inputs={[a, c]}
|
||||
output={set}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
<ValidateMemoization
|
||||
inputs={[a, c]}
|
||||
output={setAlias}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
<ValidateMemoization
|
||||
inputs={[b]}
|
||||
output={[hasB]}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export {
|
||||
export {
|
||||
Effect,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
printHIR,
|
||||
printFunctionWithOutlined,
|
||||
validateEnvironmentConfig,
|
||||
|
||||
@@ -8,20 +8,14 @@
|
||||
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {z} from 'zod';
|
||||
import {compile, type PrintedCompilerPipelineValue} from './compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
PluginOptions,
|
||||
SourceLocation,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import * as cheerio from 'cheerio';
|
||||
import {queryAlgolia} from './utils/algolia';
|
||||
import assertExhaustive from './utils/assertExhaustive';
|
||||
import {convert} from 'html-to-text';
|
||||
import {measurePerformance} from './tools/runtimePerf';
|
||||
import {parseReactComponentTree} from './tools/componentTree';
|
||||
import {
|
||||
runtimePerfTool,
|
||||
componentTreeTool,
|
||||
compileTool,
|
||||
devDocsTool,
|
||||
} from './tools';
|
||||
export type {PassNameType} from './tools';
|
||||
|
||||
function calculateMean(values: number[]): string {
|
||||
return values.length > 0
|
||||
@@ -41,38 +35,27 @@ server.tool(
|
||||
query: z.string(),
|
||||
},
|
||||
async ({query}) => {
|
||||
try {
|
||||
const pages = await queryAlgolia(query);
|
||||
if (pages.length === 0) {
|
||||
const result = await devDocsTool(query);
|
||||
|
||||
switch (result.kind) {
|
||||
case 'success': {
|
||||
return {
|
||||
content: [{type: 'text' as const, text: `No results`}],
|
||||
isError: false,
|
||||
content: result.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: text,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
const content = pages.map(html => {
|
||||
const $ = cheerio.load(html);
|
||||
// react.dev should always have at least one <article> with the main content
|
||||
const article = $('article').html();
|
||||
if (article != null) {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: convert(article),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
// Fallback to converting the whole page to text.
|
||||
text: convert($.html()),
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
content,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: result.text}],
|
||||
};
|
||||
default:
|
||||
assertExhaustive(result, `Unhandled result ${JSON.stringify(result)}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -93,199 +76,47 @@ server.tool(
|
||||
passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(),
|
||||
},
|
||||
async ({text, passName}) => {
|
||||
const pipelinePasses = new Map<
|
||||
string,
|
||||
Array<PrintedCompilerPipelineValue>
|
||||
>();
|
||||
const recordPass: (
|
||||
result: PrintedCompilerPipelineValue,
|
||||
) => void = result => {
|
||||
const entry = pipelinePasses.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
pipelinePasses.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
recordPass({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
recordPass({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
recordPass({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(result, `Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
|
||||
const compilerOptions: Partial<PluginOptions> = {
|
||||
panicThreshold: 'none',
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename, event): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
const detail = event.detail;
|
||||
const loc =
|
||||
detail.loc == null || typeof detail.loc == 'symbol'
|
||||
? event.fnLoc
|
||||
: detail.loc;
|
||||
errors.push({
|
||||
message: detail.reason,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await compile({
|
||||
text,
|
||||
file: 'anonymous.tsx',
|
||||
options: compilerOptions,
|
||||
});
|
||||
if (result.code == null) {
|
||||
const results = await compileTool(text, passName);
|
||||
|
||||
switch (results.kind) {
|
||||
case 'success': {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: 'Error: Could not compile'}],
|
||||
};
|
||||
}
|
||||
const requestedPasses: Array<{type: 'text'; text: string}> = [];
|
||||
if (passName != null) {
|
||||
switch (passName) {
|
||||
case 'All': {
|
||||
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
|
||||
if (hir !== undefined) {
|
||||
for (const pipelineValue of hir) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (reactiveFunc !== undefined) {
|
||||
for (const pipelineValue of reactiveFunc) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'HIR': {
|
||||
// Last pass before HIR -> ReactiveFunction
|
||||
const requestedPass = pipelinePasses.get(
|
||||
'PropagateScopeDependenciesHIR',
|
||||
);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ReactiveFunction': {
|
||||
// Last pass
|
||||
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '@DEBUG': {
|
||||
for (const [, pipelinePass] of pipelinePasses) {
|
||||
for (const pass of pipelinePass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: `${pass.name}\n\n${pass.value}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
passName,
|
||||
`Unhandled passName option: ${passName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const requestedPass = pipelinePasses.get(passName);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
if (pipelineValue.name === passName) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
return {
|
||||
content: errors.map(err => {
|
||||
isError: false,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text:
|
||||
err.loc === null || typeof err.loc === 'symbol'
|
||||
? `React Compiler bailed out:\n\n${err.message}`
|
||||
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{type: 'text' as const, text: result.code},
|
||||
...requestedPasses,
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
case 'bailout': {
|
||||
return {
|
||||
isError: true,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'error':
|
||||
case 'compile-error':
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: results.text,
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
assertExhaustive(
|
||||
results,
|
||||
`Unhandled result ${JSON.stringify(results)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -328,7 +159,7 @@ server.tool(
|
||||
},
|
||||
async ({text, iterations}) => {
|
||||
try {
|
||||
const results = await measurePerformance(text, iterations);
|
||||
const results = await runtimePerfTool(text, iterations);
|
||||
const formattedResults = `
|
||||
# React Component Performance Results
|
||||
|
||||
@@ -387,7 +218,7 @@ server.tool(
|
||||
},
|
||||
async ({url}) => {
|
||||
try {
|
||||
const componentTree = await parseReactComponentTree(url);
|
||||
const componentTree = await componentTreeTool(url);
|
||||
|
||||
return {
|
||||
content: [
|
||||
@@ -491,7 +322,17 @@ async function main() {
|
||||
console.error('React Compiler MCP Server running on stdio');
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Fatal error in main():', error);
|
||||
process.exit(1);
|
||||
});
|
||||
if (require.main !== module) {
|
||||
main().catch(error => {
|
||||
console.error('Fatal error in main():', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
compileTool,
|
||||
componentTreeTool,
|
||||
devDocsTool,
|
||||
runtimePerfTool,
|
||||
assertExhaustive,
|
||||
};
|
||||
|
||||
203
compiler/packages/react-mcp-server/src/tools/compileTool.ts
Normal file
203
compiler/packages/react-mcp-server/src/tools/compileTool.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import {compile, type PrintedCompilerPipelineValue} from '../compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
PluginOptions,
|
||||
SourceLocation,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import assertExhaustive from '../utils/assertExhaustive';
|
||||
|
||||
export type PassNameType =
|
||||
| 'HIR'
|
||||
| 'ReactiveFunction'
|
||||
| 'All'
|
||||
| '@DEBUG'
|
||||
| undefined;
|
||||
|
||||
type CompilerToolOutput =
|
||||
| {
|
||||
kind: 'success';
|
||||
content: Array<string>;
|
||||
}
|
||||
| {
|
||||
kind: 'bailout';
|
||||
content: Array<string>;
|
||||
}
|
||||
| {
|
||||
kind: 'compile-error';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
kind: 'error';
|
||||
text: string;
|
||||
};
|
||||
|
||||
export async function compileTool(
|
||||
text: string,
|
||||
passName: PassNameType,
|
||||
): Promise<CompilerToolOutput> {
|
||||
const pipelinePasses = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const recordPass: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = pipelinePasses.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
pipelinePasses.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
recordPass({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
recordPass({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
recordPass({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(result, `Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
|
||||
const compilerOptions: Partial<PluginOptions> = {
|
||||
panicThreshold: 'none',
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename, event): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
const detail = event.detail;
|
||||
const loc =
|
||||
detail.loc == null || typeof detail.loc == 'symbol'
|
||||
? event.fnLoc
|
||||
: detail.loc;
|
||||
errors.push({
|
||||
message: detail.reason,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await compile({
|
||||
text,
|
||||
file: 'anonymous.tsx',
|
||||
options: compilerOptions,
|
||||
});
|
||||
if (result.code == null) {
|
||||
return {
|
||||
kind: 'compile-error',
|
||||
text: 'Error: Could not compile',
|
||||
};
|
||||
}
|
||||
const requestedPasses: Array<string> = [];
|
||||
if (passName != null) {
|
||||
switch (passName) {
|
||||
case 'All': {
|
||||
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
|
||||
if (hir !== undefined) {
|
||||
for (const pipelineValue of hir) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
}
|
||||
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (reactiveFunc !== undefined) {
|
||||
for (const pipelineValue of reactiveFunc) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'HIR': {
|
||||
// Last pass before HIR -> ReactiveFunction
|
||||
const requestedPass = pipelinePasses.get(
|
||||
'PropagateScopeDependenciesHIR',
|
||||
);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ReactiveFunction': {
|
||||
// Last pass
|
||||
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '@DEBUG': {
|
||||
for (const [, pipelinePass] of pipelinePasses) {
|
||||
for (const pass of pipelinePass) {
|
||||
requestedPasses.push(`${pass.name}\n\n${pass.value}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(passName, `Unhandled passName option: ${passName}`);
|
||||
}
|
||||
}
|
||||
const requestedPass = pipelinePasses.get(passName);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
if (pipelineValue.name === passName) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
return {
|
||||
kind: 'bailout',
|
||||
content: errors.map(err => {
|
||||
return err.loc === null || typeof err.loc === 'symbol'
|
||||
? `React Compiler bailed out:\n\n${err.message}`
|
||||
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`;
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: 'success',
|
||||
content: [result.code, ...requestedPasses],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
kind: 'error',
|
||||
text: `Error: ${err.stack}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
export async function parseReactComponentTree(url: string): Promise<string> {
|
||||
export async function componentTreeTool(url: string): Promise<string> {
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: 'http://127.0.0.1:9222',
|
||||
49
compiler/packages/react-mcp-server/src/tools/devDocsTool.ts
Normal file
49
compiler/packages/react-mcp-server/src/tools/devDocsTool.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import {convert} from 'html-to-text';
|
||||
import {queryAlgolia} from '../utils/algolia';
|
||||
|
||||
type DevDocsToolOutput =
|
||||
| {
|
||||
kind: 'success';
|
||||
content: Array<string>;
|
||||
}
|
||||
| {
|
||||
kind: 'error';
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool for querying React dev docs from react.dev
|
||||
* @param query The search query to look up in the React documentation
|
||||
* @returns A promise that resolves to the search results
|
||||
*/
|
||||
export async function devDocsTool(query: string): Promise<DevDocsToolOutput> {
|
||||
try {
|
||||
const pages = await queryAlgolia(query);
|
||||
if (pages.length === 0) {
|
||||
return {
|
||||
kind: 'error',
|
||||
text: `No results`,
|
||||
};
|
||||
}
|
||||
const content = pages.map(html => {
|
||||
const $ = cheerio.load(html);
|
||||
// react.dev should always have at least one <article> with the main content
|
||||
const article = $('article').html();
|
||||
if (article != null) {
|
||||
return convert(article);
|
||||
} else {
|
||||
return convert($.html());
|
||||
}
|
||||
});
|
||||
return {
|
||||
kind: 'success',
|
||||
content,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
kind: 'error',
|
||||
text: `Error: ${err.stack}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
11
compiler/packages/react-mcp-server/src/tools/index.ts
Normal file
11
compiler/packages/react-mcp-server/src/tools/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export * from './runtimePerfTool';
|
||||
export * from './componentTreeTool';
|
||||
export * from './compileTool';
|
||||
export * from './devDocsTool';
|
||||
@@ -53,7 +53,7 @@ function delay(time: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function measurePerformance(
|
||||
export async function runtimePerfTool(
|
||||
code: string,
|
||||
iterations: number,
|
||||
): Promise<PerformanceResults> {
|
||||
@@ -69,23 +69,27 @@ export async function measurePerformance(
|
||||
throw new Error('Failed to parse code');
|
||||
}
|
||||
|
||||
const transformResult = await babel.transformFromAstAsync(parsed, undefined, {
|
||||
...babelOptions,
|
||||
plugins: [
|
||||
() => ({
|
||||
visitor: {
|
||||
ImportDeclaration(
|
||||
path: babel.NodePath<babel.types.ImportDeclaration>,
|
||||
) {
|
||||
const value = path.node.source.value;
|
||||
if (value === 'react' || value === 'react-dom') {
|
||||
path.remove();
|
||||
}
|
||||
const transformResult = await babel.transformFromAstAsync(
|
||||
parsed as babel.types.Node,
|
||||
undefined,
|
||||
{
|
||||
...babelOptions,
|
||||
plugins: [
|
||||
() => ({
|
||||
visitor: {
|
||||
ImportDeclaration(
|
||||
path: babel.NodePath<babel.types.ImportDeclaration>,
|
||||
) {
|
||||
const value = path.node.source.value;
|
||||
if (value === 'react' || value === 'react-dom') {
|
||||
path.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const transpiled = transformResult?.code || undefined;
|
||||
if (!transpiled) {
|
||||
@@ -125,14 +129,16 @@ export async function measurePerformance(
|
||||
|
||||
// ui chaos monkey
|
||||
const selectors = await page.evaluate(() => {
|
||||
window.__INTERACTABLE_SELECTORS__ = [];
|
||||
(window as any).__INTERACTABLE_SELECTORS__ = [];
|
||||
const elements = Array.from(document.querySelectorAll('a')).concat(
|
||||
Array.from(document.querySelectorAll('button')),
|
||||
Array.from(document.querySelectorAll('button')) as any,
|
||||
);
|
||||
for (const el of elements) {
|
||||
window.__INTERACTABLE_SELECTORS__.push(el.tagName.toLowerCase());
|
||||
(window as any).__INTERACTABLE_SELECTORS__.push(
|
||||
el.tagName.toLowerCase(),
|
||||
);
|
||||
}
|
||||
return window.__INTERACTABLE_SELECTORS__;
|
||||
return (window as any).__INTERACTABLE_SELECTORS__;
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
38
compiler/packages/react-tools-cli/package.json
Normal file
38
compiler/packages/react-tools-cli/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "react-tools-cli",
|
||||
"version": "0.0.0",
|
||||
"description": "CLI to execute react-mcp-server tools in isolation from the full mcp server",
|
||||
"bin": {
|
||||
"react-tools-cli": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf dist && tsup"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/facebook/react.git",
|
||||
"directory": "compiler/packages/react-tools-cli"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/parser": "^7.26",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"algoliasearch": "^5.23.3",
|
||||
"cheerio": "^1.0.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"prettier": "^3.3.3",
|
||||
"puppeteer": "^24.7.2",
|
||||
"yargs": "^18.0.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.3.2"
|
||||
}
|
||||
}
|
||||
240
compiler/packages/react-tools-cli/src/index.ts
Normal file
240
compiler/packages/react-tools-cli/src/index.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
PassNameType,
|
||||
assertExhaustive,
|
||||
compileTool,
|
||||
componentTreeTool,
|
||||
devDocsTool,
|
||||
runtimePerfTool,
|
||||
} from 'react-mcp-server/src';
|
||||
|
||||
import yargs from 'yargs';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName('react-tools-cli')
|
||||
.usage('$0 <cmd> [args]')
|
||||
.command(
|
||||
'compile [code] [pass-name]',
|
||||
'Compile React code with React Compiler',
|
||||
yargs => {
|
||||
yargs
|
||||
.positional('code', {
|
||||
type: 'string',
|
||||
describe: 'The code to compile',
|
||||
})
|
||||
.option('pass-name', {
|
||||
type: 'string',
|
||||
choices: ['HIR', 'ReactiveFunction', 'All', '@DEBUG'],
|
||||
describe: 'Compiler pass to run',
|
||||
});
|
||||
},
|
||||
async function (argv) {
|
||||
const code: string = String(argv['code'] ?? '');
|
||||
const passName: PassNameType = argv['pass-name'] as PassNameType;
|
||||
|
||||
const results = await compileTool(code, passName);
|
||||
|
||||
switch (results.kind) {
|
||||
case 'success': {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: false,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'bailout': {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
case 'error':
|
||||
case 'compile-error':
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: results.text,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
process.exit(1);
|
||||
default:
|
||||
assertExhaustive(
|
||||
results,
|
||||
`Unhandled result ${JSON.stringify(results)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.command(
|
||||
'query-docs [query]',
|
||||
'Compile React code with React Compiler',
|
||||
yargs => {
|
||||
yargs.positional('query', {
|
||||
type: 'string',
|
||||
describe: 'Browse oficcial React documentation for a given query',
|
||||
});
|
||||
},
|
||||
async function (argv) {
|
||||
const query: string = String(argv['query'] ?? '');
|
||||
|
||||
const result = await devDocsTool(query);
|
||||
|
||||
switch (result.kind) {
|
||||
case 'success':
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: false,
|
||||
content: result.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: text,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'error':
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: result.text}],
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertExhaustive(
|
||||
result,
|
||||
`Unhandled result ${JSON.stringify(result)}`,
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
},
|
||||
)
|
||||
.command(
|
||||
'get-component-tree [url]',
|
||||
'Get the React component tree for a given URL',
|
||||
yargs => {
|
||||
yargs.positional('url', {
|
||||
type: 'string',
|
||||
default: 'https://localhost:3000',
|
||||
describe: 'URL for a React App to get the component tree for',
|
||||
});
|
||||
},
|
||||
async function (argv) {
|
||||
const url: string = String(argv['url']);
|
||||
try {
|
||||
const result = await componentTreeTool(url);
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
}),
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
},
|
||||
)
|
||||
.command(
|
||||
'review-code-runtime [code] [iterations',
|
||||
'Get the React component tree for a given URL',
|
||||
yargs => {
|
||||
yargs.positional('code', {
|
||||
type: 'string',
|
||||
default: '',
|
||||
describe: 'React code to run',
|
||||
});
|
||||
yargs.positional('iterations', {
|
||||
type: 'number',
|
||||
default: 10,
|
||||
describe: 'Number of iterations to run the code for',
|
||||
});
|
||||
},
|
||||
async function (argv) {
|
||||
const code: string = String(argv['code']);
|
||||
const iterations: number = Number(argv['iterations']);
|
||||
|
||||
try {
|
||||
const results = await runtimePerfTool(code, iterations);
|
||||
|
||||
const formattedResults = `
|
||||
# React Component Performance Results
|
||||
|
||||
## Mean Render Time
|
||||
${calculateMean(results.renderTime)}
|
||||
|
||||
## Mean Web Vitals
|
||||
- Cumulative Layout Shift (CLS): ${calculateMean(results.webVitals.cls)}
|
||||
- Largest Contentful Paint (LCP): ${calculateMean(results.webVitals.lcp)}
|
||||
- Interaction to Next Paint (INP): ${calculateMean(results.webVitals.inp)}
|
||||
|
||||
## Mean React Profiler
|
||||
- Actual Duration: ${calculateMean(results.reactProfiler.actualDuration)}
|
||||
- Base Duration: ${calculateMean(results.reactProfiler.baseDuration)}
|
||||
`;
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: formattedResults,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: `Error measuring performance: ${error.message}\n\n${error.stack}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
},
|
||||
)
|
||||
.help()
|
||||
.parse();
|
||||
|
||||
function calculateMean(values: number[]): string {
|
||||
return values.length > 0
|
||||
? values.reduce((acc, curr) => acc + curr, 0) / values.length + 'ms'
|
||||
: 'could not collect';
|
||||
}
|
||||
22
compiler/packages/react-tools-cli/tsconfig.json
Normal file
22
compiler/packages/react-tools-cli/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"rootDir": "../",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsxdev",
|
||||
"lib": ["ES2022"],
|
||||
|
||||
// weaken strictness from preset
|
||||
"importsNotUsedAsValues": "remove",
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"noUnusedParameters": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"target": "ES2022",
|
||||
// ideally turn off only during dev, or on a per-file basis
|
||||
"noUnusedLocals": false,
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["src/**/*.ts"],
|
||||
}
|
||||
37
compiler/packages/react-tools-cli/tsup.config.ts
Normal file
37
compiler/packages/react-tools-cli/tsup.config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {defineConfig} from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['./src/index.ts'],
|
||||
outDir: './dist',
|
||||
external: [],
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
dts: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
target: 'es2022',
|
||||
banner: {
|
||||
js: `#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/`,
|
||||
},
|
||||
});
|
||||
@@ -18,7 +18,11 @@ import type {
|
||||
CompilerReactTarget,
|
||||
CompilerPipelineValue,
|
||||
} from 'babel-plugin-react-compiler/src/Entrypoint';
|
||||
import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR';
|
||||
import type {
|
||||
Effect,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from 'babel-plugin-react-compiler/src/HIR';
|
||||
import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import invariant from 'invariant';
|
||||
@@ -42,6 +46,7 @@ function makePluginOptions(
|
||||
debugIRLogger: (value: CompilerPipelineValue) => void,
|
||||
EffectEnum: typeof Effect,
|
||||
ValueKindEnum: typeof ValueKind,
|
||||
ValueReasonEnum: typeof ValueReason,
|
||||
): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] {
|
||||
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
|
||||
let validatePreserveExistingMemoizationGuarantees = false;
|
||||
@@ -77,6 +82,7 @@ function makePluginOptions(
|
||||
moduleTypeProvider: makeSharedRuntimeTypeProvider({
|
||||
EffectEnum,
|
||||
ValueKindEnum,
|
||||
ValueReasonEnum,
|
||||
}),
|
||||
assertValidMutableRanges: true,
|
||||
validatePreserveExistingMemoizationGuarantees,
|
||||
@@ -209,6 +215,7 @@ export async function transformFixtureInput(
|
||||
debugIRLogger: (value: CompilerPipelineValue) => void,
|
||||
EffectEnum: typeof Effect,
|
||||
ValueKindEnum: typeof ValueKind,
|
||||
ValueReasonEnum: typeof ValueReason,
|
||||
): Promise<{kind: 'ok'; value: TransformResult} | {kind: 'err'; msg: string}> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const firstLine = input.substring(0, input.indexOf('\n'));
|
||||
@@ -237,6 +244,7 @@ export async function transformFixtureInput(
|
||||
debugIRLogger,
|
||||
EffectEnum,
|
||||
ValueKindEnum,
|
||||
ValueReasonEnum,
|
||||
);
|
||||
const forgetResult = transformFromAstSync(inputAst, input, {
|
||||
filename: virtualFilepath,
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
CompilerPipelineValue,
|
||||
Effect,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -78,6 +79,9 @@ async function compile(
|
||||
const ValueKindEnum = importedCompilerPlugin[
|
||||
'ValueKind'
|
||||
] as typeof ValueKind;
|
||||
const ValueReasonEnum = importedCompilerPlugin[
|
||||
'ValueReason'
|
||||
] as typeof ValueReason;
|
||||
const printFunctionWithOutlined = importedCompilerPlugin[
|
||||
PRINT_HIR_IMPORT
|
||||
] as typeof PrintFunctionWithOutlined;
|
||||
@@ -128,6 +132,7 @@ async function compile(
|
||||
debugIRLogger,
|
||||
EffectEnum,
|
||||
ValueKindEnum,
|
||||
ValueReasonEnum,
|
||||
);
|
||||
|
||||
if (result.kind === 'err') {
|
||||
|
||||
@@ -5,15 +5,21 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src';
|
||||
import type {
|
||||
Effect,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import type {TypeConfig} from 'babel-plugin-react-compiler/src/HIR/TypeSchema';
|
||||
|
||||
export function makeSharedRuntimeTypeProvider({
|
||||
EffectEnum,
|
||||
ValueKindEnum,
|
||||
ValueReasonEnum,
|
||||
}: {
|
||||
EffectEnum: typeof Effect;
|
||||
ValueKindEnum: typeof ValueKind;
|
||||
ValueReasonEnum: typeof ValueReason;
|
||||
}) {
|
||||
return function sharedRuntimeTypeProvider(
|
||||
moduleName: string,
|
||||
@@ -69,6 +75,127 @@ export function makeSharedRuntimeTypeProvider({
|
||||
returnValueKind: ValueKindEnum.Mutable,
|
||||
noAlias: true,
|
||||
},
|
||||
typedIdentity: {
|
||||
kind: 'function',
|
||||
positionalParams: [EffectEnum.Read],
|
||||
restParam: null,
|
||||
calleeEffect: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
returnValueKind: ValueKindEnum.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@value'],
|
||||
rest: null,
|
||||
returns: '@return',
|
||||
temporaries: [],
|
||||
effects: [{kind: 'Assign', from: '@value', into: '@return'}],
|
||||
},
|
||||
},
|
||||
typedAssign: {
|
||||
kind: 'function',
|
||||
positionalParams: [EffectEnum.Read],
|
||||
restParam: null,
|
||||
calleeEffect: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
returnValueKind: ValueKindEnum.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@value'],
|
||||
rest: null,
|
||||
returns: '@return',
|
||||
temporaries: [],
|
||||
effects: [{kind: 'Assign', from: '@value', into: '@return'}],
|
||||
},
|
||||
},
|
||||
typedAlias: {
|
||||
kind: 'function',
|
||||
positionalParams: [EffectEnum.Read],
|
||||
restParam: null,
|
||||
calleeEffect: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
returnValueKind: ValueKindEnum.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@value'],
|
||||
rest: null,
|
||||
returns: '@return',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@return',
|
||||
value: ValueKindEnum.Mutable,
|
||||
reason: ValueReasonEnum.KnownReturnSignature,
|
||||
},
|
||||
{kind: 'Alias', from: '@value', into: '@return'},
|
||||
],
|
||||
},
|
||||
},
|
||||
typedCapture: {
|
||||
kind: 'function',
|
||||
positionalParams: [EffectEnum.Read],
|
||||
restParam: null,
|
||||
calleeEffect: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Array'},
|
||||
returnValueKind: ValueKindEnum.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@value'],
|
||||
rest: null,
|
||||
returns: '@return',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@return',
|
||||
value: ValueKindEnum.Mutable,
|
||||
reason: ValueReasonEnum.KnownReturnSignature,
|
||||
},
|
||||
{kind: 'Capture', from: '@value', into: '@return'},
|
||||
],
|
||||
},
|
||||
},
|
||||
typedCreateFrom: {
|
||||
kind: 'function',
|
||||
positionalParams: [EffectEnum.Read],
|
||||
restParam: null,
|
||||
calleeEffect: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
returnValueKind: ValueKindEnum.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@value'],
|
||||
rest: null,
|
||||
returns: '@return',
|
||||
temporaries: [],
|
||||
effects: [{kind: 'CreateFrom', from: '@value', into: '@return'}],
|
||||
},
|
||||
},
|
||||
typedMutate: {
|
||||
kind: 'function',
|
||||
positionalParams: [EffectEnum.Read, EffectEnum.Capture],
|
||||
restParam: null,
|
||||
calleeEffect: EffectEnum.Store,
|
||||
returnType: {kind: 'type', name: 'Primitive'},
|
||||
returnValueKind: ValueKindEnum.Primitive,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@object', '@value'],
|
||||
rest: null,
|
||||
returns: '@return',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@return',
|
||||
value: ValueKindEnum.Primitive,
|
||||
reason: ValueReasonEnum.KnownReturnSignature,
|
||||
},
|
||||
{kind: 'Mutate', value: '@object'},
|
||||
{kind: 'Capture', from: '@value', into: '@object'},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (moduleName === 'ReactCompilerTest') {
|
||||
|
||||
@@ -272,7 +272,7 @@ export function ValidateMemoization({
|
||||
}: {
|
||||
inputs: Array<any>;
|
||||
output: any;
|
||||
onlyCheckCompiled: boolean;
|
||||
onlyCheckCompiled?: boolean;
|
||||
}): React.ReactElement {
|
||||
'use no forget';
|
||||
// Wrap rawOutput as it might be a function, which useState would invoke.
|
||||
@@ -280,8 +280,9 @@ export function ValidateMemoization({
|
||||
const [previousInputs, setPreviousInputs] = React.useState(inputs);
|
||||
const [previousOutput, setPreviousOutput] = React.useState(output);
|
||||
if (
|
||||
onlyCheckCompiled &&
|
||||
(globalThis as any).__SNAP_EVALUATOR_MODE === 'forget'
|
||||
!onlyCheckCompiled ||
|
||||
(onlyCheckCompiled &&
|
||||
(globalThis as any).__SNAP_EVALUATOR_MODE === 'forget')
|
||||
) {
|
||||
if (
|
||||
inputs.length !== previousInputs.length ||
|
||||
@@ -396,4 +397,28 @@ export function typedLog(...values: Array<any>): void {
|
||||
console.log(...values);
|
||||
}
|
||||
|
||||
export function typedIdentity<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function typedAssign<T>(x: T): T {
|
||||
return x;
|
||||
}
|
||||
|
||||
export function typedAlias<T>(x: T): T {
|
||||
return x;
|
||||
}
|
||||
|
||||
export function typedCapture<T>(x: T): Array<T> {
|
||||
return [x];
|
||||
}
|
||||
|
||||
export function typedCreateFrom<T>(array: Array<T>): T {
|
||||
return array[0];
|
||||
}
|
||||
|
||||
export function typedMutate(x: any, v: any = null): void {
|
||||
x.property = v;
|
||||
}
|
||||
|
||||
export default typedLog;
|
||||
|
||||
@@ -542,11 +542,6 @@
|
||||
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz"
|
||||
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
|
||||
|
||||
"@babel/helper-string-parser@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
||||
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz"
|
||||
@@ -1605,7 +1600,7 @@
|
||||
debug "^4.3.1"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
|
||||
"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
|
||||
version "7.26.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
|
||||
integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
|
||||
@@ -1613,14 +1608,6 @@
|
||||
"@babel/helper-string-parser" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
|
||||
"@babel/types@^7.26.10", "@babel/types@^7.27.0", "@babel/types@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560"
|
||||
integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
|
||||
"@bcoe/v8-coverage@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
|
||||
@@ -3745,7 +3732,7 @@ ansi-styles@^5.0.0:
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz"
|
||||
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||
|
||||
ansi-styles@^6.1.0:
|
||||
ansi-styles@^6.1.0, ansi-styles@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
@@ -4435,6 +4422,15 @@ cliui@^8.0.1:
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cliui@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-9.0.1.tgz#6f7890f386f6f1f79953adc1f78dec46fcc2d291"
|
||||
integrity sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==
|
||||
dependencies:
|
||||
string-width "^7.2.0"
|
||||
strip-ansi "^7.1.0"
|
||||
wrap-ansi "^9.0.0"
|
||||
|
||||
clone-deep@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz"
|
||||
@@ -4963,7 +4959,7 @@ emittery@^0.13.1:
|
||||
resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz"
|
||||
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
|
||||
|
||||
emoji-regex@^10.2.1:
|
||||
emoji-regex@^10.2.1, emoji-regex@^10.3.0:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz"
|
||||
integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
|
||||
@@ -5798,6 +5794,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-east-asian-width@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389"
|
||||
integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==
|
||||
|
||||
get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
|
||||
@@ -9825,6 +9826,15 @@ string-width@^6.1.0:
|
||||
emoji-regex "^10.2.1"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
string-width@^7.0.0, string-width@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc"
|
||||
integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==
|
||||
dependencies:
|
||||
emoji-regex "^10.3.0"
|
||||
get-east-asian-width "^1.0.0"
|
||||
strip-ansi "^7.1.0"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
|
||||
@@ -10551,6 +10561,15 @@ wrap-ansi@^8.1.0:
|
||||
string-width "^5.0.1"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
wrap-ansi@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e"
|
||||
integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==
|
||||
dependencies:
|
||||
ansi-styles "^6.2.1"
|
||||
string-width "^7.0.0"
|
||||
strip-ansi "^7.1.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
|
||||
@@ -10617,6 +10636,11 @@ yargs-parser@^21.0.1, yargs-parser@^21.1.1:
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs-parser@^22.0.0:
|
||||
version "22.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-22.0.0.tgz#87b82094051b0567717346ecd00fd14804b357c8"
|
||||
integrity sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==
|
||||
|
||||
yargs-unparser@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz"
|
||||
@@ -10670,6 +10694,18 @@ yargs@^17.3.1, yargs@^17.7.1, yargs@^17.7.2:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
yargs@^18.0.0:
|
||||
version "18.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-18.0.0.tgz#6c84259806273a746b09f579087b68a3c2d25bd1"
|
||||
integrity sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==
|
||||
dependencies:
|
||||
cliui "^9.0.1"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
string-width "^7.2.0"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^22.0.0"
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"
|
||||
|
||||
@@ -104,6 +104,9 @@ async function renderApp(req, res, next) {
|
||||
if (req.headers['cache-control']) {
|
||||
proxiedHeaders['Cache-Control'] = req.get('cache-control');
|
||||
}
|
||||
if (req.get('rsc-request-id')) {
|
||||
proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id');
|
||||
}
|
||||
|
||||
const requestsPrerender = req.path === '/prerender';
|
||||
|
||||
|
||||
@@ -50,7 +50,27 @@ const {readFile} = require('fs').promises;
|
||||
|
||||
const React = require('react');
|
||||
|
||||
async function renderApp(res, returnValue, formState, noCache) {
|
||||
const activeDebugChannels =
|
||||
process.env.NODE_ENV === 'development' ? new Map() : null;
|
||||
|
||||
function getDebugChannel(req) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
return undefined;
|
||||
}
|
||||
const requestId = req.get('rsc-request-id');
|
||||
if (!requestId) {
|
||||
return undefined;
|
||||
}
|
||||
return activeDebugChannels.get(requestId);
|
||||
}
|
||||
|
||||
async function renderApp(
|
||||
res,
|
||||
returnValue,
|
||||
formState,
|
||||
noCache,
|
||||
promiseForDebugChannel
|
||||
) {
|
||||
const {renderToPipeableStream} = await import(
|
||||
'react-server-dom-webpack/server'
|
||||
);
|
||||
@@ -101,7 +121,9 @@ async function renderApp(res, returnValue, formState, noCache) {
|
||||
);
|
||||
// For client-invoked server actions we refresh the tree and return a return value.
|
||||
const payload = {root, returnValue, formState};
|
||||
const {pipe} = renderToPipeableStream(payload, moduleMap);
|
||||
const {pipe} = renderToPipeableStream(payload, moduleMap, {
|
||||
debugChannel: await promiseForDebugChannel,
|
||||
});
|
||||
pipe(res);
|
||||
}
|
||||
|
||||
@@ -166,7 +188,7 @@ app.get('/', async function (req, res) {
|
||||
if ('prerender' in req.query) {
|
||||
await prerenderApp(res, null, null, noCache);
|
||||
} else {
|
||||
await renderApp(res, null, null, noCache);
|
||||
await renderApp(res, null, null, noCache, getDebugChannel(req));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -204,7 +226,7 @@ app.post('/', bodyParser.text(), async function (req, res) {
|
||||
// We handle the error on the client
|
||||
}
|
||||
// Refresh the client and return the value
|
||||
renderApp(res, result, null, noCache);
|
||||
renderApp(res, result, null, noCache, getDebugChannel(req));
|
||||
} else {
|
||||
// This is the progressive enhancement case
|
||||
const UndiciRequest = require('undici').Request;
|
||||
@@ -220,11 +242,11 @@ app.post('/', bodyParser.text(), async function (req, res) {
|
||||
// Wait for any mutations
|
||||
const result = await action();
|
||||
const formState = decodeFormState(result, formData);
|
||||
renderApp(res, null, formState, noCache);
|
||||
renderApp(res, null, formState, noCache, undefined);
|
||||
} catch (x) {
|
||||
const {setServerState} = await import('../src/ServerState.js');
|
||||
setServerState('Error: ' + x.message);
|
||||
renderApp(res, null, null, noCache);
|
||||
renderApp(res, null, null, noCache, undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -324,7 +346,7 @@ if (process.env.NODE_ENV === 'development') {
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(3001, () => {
|
||||
const httpServer = app.listen(3001, () => {
|
||||
console.log('Regional Flight Server listening on port 3001...');
|
||||
});
|
||||
|
||||
@@ -346,3 +368,27 @@ app.on('error', function (error) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Open a websocket server for Debug information
|
||||
const WebSocket = require('ws');
|
||||
const webSocketServer = new WebSocket.Server({noServer: true});
|
||||
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
const DEBUG_CHANNEL_PATH = '/debug-channel?';
|
||||
if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
|
||||
const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
|
||||
const promiseForWs = new Promise(resolve => {
|
||||
webSocketServer.handleUpgrade(request, socket, head, ws => {
|
||||
ws.on('close', () => {
|
||||
activeDebugChannels.delete(requestId);
|
||||
});
|
||||
resolve(ws);
|
||||
});
|
||||
});
|
||||
activeDebugChannels.set(requestId, promiseForWs);
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,12 +33,22 @@ function Foo({children}) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
async function delayedError(text, ms) {
|
||||
return new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(text)), ms)
|
||||
);
|
||||
}
|
||||
|
||||
async function delay(text, ms) {
|
||||
return new Promise(resolve => setTimeout(() => resolve(text), ms));
|
||||
}
|
||||
|
||||
async function delayTwice() {
|
||||
await delay('', 20);
|
||||
try {
|
||||
await delayedError('Delayed exception', 20);
|
||||
} catch (x) {
|
||||
// Ignored
|
||||
}
|
||||
await delay('', 10);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,17 +42,43 @@ function Shell({data}) {
|
||||
}
|
||||
|
||||
async function hydrateApp() {
|
||||
const {root, returnValue, formState} = await createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
},
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
}
|
||||
);
|
||||
let response;
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
typeof WebSocketStream === 'function'
|
||||
) {
|
||||
const requestId = crypto.randomUUID();
|
||||
const wss = new WebSocketStream(
|
||||
'ws://localhost:3001/debug-channel?' + requestId
|
||||
);
|
||||
const debugChannel = await wss.opened;
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
'rsc-request-id': requestId,
|
||||
},
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
debugChannel,
|
||||
findSourceMapURL,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
},
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
}
|
||||
);
|
||||
}
|
||||
const {root, returnValue, formState} = await response;
|
||||
|
||||
ReactDOM.hydrateRoot(
|
||||
document,
|
||||
|
||||
@@ -4,7 +4,6 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
use,
|
||||
} from 'react';
|
||||
|
||||
import Chrome from './Chrome';
|
||||
|
||||
362
packages/react-client/src/ReactFlightClient.js
vendored
362
packages/react-client/src/ReactFlightClient.js
vendored
@@ -79,7 +79,9 @@ import {
|
||||
logDedupedComponentRender,
|
||||
logComponentErrored,
|
||||
logIOInfo,
|
||||
logIOInfoErrored,
|
||||
logComponentAwait,
|
||||
logComponentAwaitErrored,
|
||||
} from './ReactFlightPerformanceTrack';
|
||||
|
||||
import {
|
||||
@@ -96,6 +98,8 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack'
|
||||
|
||||
import {injectInternals} from './ReactFlightClientDevToolsHook';
|
||||
|
||||
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
|
||||
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
|
||||
import isArray from 'shared/isArray';
|
||||
@@ -155,12 +159,12 @@ const RESOLVED_MODEL = 'resolved_model';
|
||||
const RESOLVED_MODULE = 'resolved_module';
|
||||
const INITIALIZED = 'fulfilled';
|
||||
const ERRORED = 'rejected';
|
||||
const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes.
|
||||
|
||||
type PendingChunk<T> = {
|
||||
status: 'pending',
|
||||
value: null | Array<(T) => mixed>,
|
||||
reason: null | Array<(mixed) => mixed>,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -169,7 +173,6 @@ type BlockedChunk<T> = {
|
||||
status: 'blocked',
|
||||
value: null | Array<(T) => mixed>,
|
||||
reason: null | Array<(mixed) => mixed>,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -177,8 +180,7 @@ type BlockedChunk<T> = {
|
||||
type ResolvedModelChunk<T> = {
|
||||
status: 'resolved_model',
|
||||
value: UninitializedModel,
|
||||
reason: null,
|
||||
_response: Response,
|
||||
reason: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -187,7 +189,6 @@ type ResolvedModuleChunk<T> = {
|
||||
status: 'resolved_module',
|
||||
value: ClientReference<T>,
|
||||
reason: null,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -196,7 +197,6 @@ type InitializedChunk<T> = {
|
||||
status: 'fulfilled',
|
||||
value: T,
|
||||
reason: null | FlightStreamController,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -207,7 +207,6 @@ type InitializedStreamChunk<
|
||||
status: 'fulfilled',
|
||||
value: T,
|
||||
reason: FlightStreamController,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -216,7 +215,14 @@ type ErroredChunk<T> = {
|
||||
status: 'rejected',
|
||||
value: null,
|
||||
reason: mixed,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
};
|
||||
type HaltedChunk<T> = {
|
||||
status: 'halted',
|
||||
value: null,
|
||||
reason: null,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -227,19 +233,14 @@ type SomeChunk<T> =
|
||||
| ResolvedModelChunk<T>
|
||||
| ResolvedModuleChunk<T>
|
||||
| InitializedChunk<T>
|
||||
| ErroredChunk<T>;
|
||||
| ErroredChunk<T>
|
||||
| HaltedChunk<T>;
|
||||
|
||||
// $FlowFixMe[missing-this-annot]
|
||||
function ReactPromise(
|
||||
status: any,
|
||||
value: any,
|
||||
reason: any,
|
||||
response: Response,
|
||||
) {
|
||||
function ReactPromise(status: any, value: any, reason: any) {
|
||||
this.status = status;
|
||||
this.value = value;
|
||||
this.reason = reason;
|
||||
this._response = response;
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
this._children = [];
|
||||
}
|
||||
@@ -266,7 +267,11 @@ ReactPromise.prototype.then = function <T>(
|
||||
initializeModuleChunk(chunk);
|
||||
break;
|
||||
}
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableAsyncDebugInfo &&
|
||||
(typeof resolve !== 'function' || !(resolve: any).isReactInternalListener)
|
||||
) {
|
||||
// Because only native Promises get picked up when we're awaiting we need to wrap
|
||||
// this in a native Promise in DEV. This means that these callbacks are no longer sync
|
||||
// but the lazy initialization is still sync and the .value can be inspected after,
|
||||
@@ -307,6 +312,9 @@ ReactPromise.prototype.then = function <T>(
|
||||
chunk.reason.push(reject);
|
||||
}
|
||||
break;
|
||||
case HALTED: {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (reject) {
|
||||
reject(chunk.reason);
|
||||
@@ -320,6 +328,8 @@ export type FindSourceMapURLCallback = (
|
||||
environmentName: string,
|
||||
) => null | string;
|
||||
|
||||
export type DebugChannelCallback = (message: string) => void;
|
||||
|
||||
export type Response = {
|
||||
_bundlerConfig: ServerConsumerModuleMap,
|
||||
_serverReferenceConfig: null | ServerManifest,
|
||||
@@ -343,6 +353,7 @@ export type Response = {
|
||||
_debugRootStack?: null | Error, // DEV-only
|
||||
_debugRootTask?: null | ConsoleTask, // DEV-only
|
||||
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
|
||||
_debugChannel?: void | DebugChannelCallback, // DEV-only
|
||||
_replayConsole: boolean, // DEV-only
|
||||
_rootEnvironmentName: string, // DEV-only, the requested environment name.
|
||||
};
|
||||
@@ -364,6 +375,7 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
|
||||
return chunk.value;
|
||||
case PENDING:
|
||||
case BLOCKED:
|
||||
case HALTED:
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw ((chunk: any): Thenable<T>);
|
||||
default:
|
||||
@@ -378,12 +390,12 @@ export function getRoot<T>(response: Response): Thenable<T> {
|
||||
|
||||
function createPendingChunk<T>(response: Response): PendingChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(PENDING, null, null, response);
|
||||
return new ReactPromise(PENDING, null, null);
|
||||
}
|
||||
|
||||
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(BLOCKED, null, null, response);
|
||||
return new ReactPromise(BLOCKED, null, null);
|
||||
}
|
||||
|
||||
function createErrorChunk<T>(
|
||||
@@ -391,7 +403,7 @@ function createErrorChunk<T>(
|
||||
error: mixed,
|
||||
): ErroredChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(ERRORED, null, error, response);
|
||||
return new ReactPromise(ERRORED, null, error);
|
||||
}
|
||||
|
||||
function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
|
||||
@@ -463,7 +475,7 @@ function createResolvedModelChunk<T>(
|
||||
value: UninitializedModel,
|
||||
): ResolvedModelChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(RESOLVED_MODEL, value, null, response);
|
||||
return new ReactPromise(RESOLVED_MODEL, value, response);
|
||||
}
|
||||
|
||||
function createResolvedModuleChunk<T>(
|
||||
@@ -471,7 +483,7 @@ function createResolvedModuleChunk<T>(
|
||||
value: ClientReference<T>,
|
||||
): ResolvedModuleChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(RESOLVED_MODULE, value, null, response);
|
||||
return new ReactPromise(RESOLVED_MODULE, value, null);
|
||||
}
|
||||
|
||||
function createInitializedTextChunk(
|
||||
@@ -479,7 +491,7 @@ function createInitializedTextChunk(
|
||||
value: string,
|
||||
): InitializedChunk<string> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(INITIALIZED, value, null, response);
|
||||
return new ReactPromise(INITIALIZED, value, null);
|
||||
}
|
||||
|
||||
function createInitializedBufferChunk(
|
||||
@@ -487,7 +499,7 @@ function createInitializedBufferChunk(
|
||||
value: $ArrayBufferView | ArrayBuffer,
|
||||
): InitializedChunk<Uint8Array> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(INITIALIZED, value, null, response);
|
||||
return new ReactPromise(INITIALIZED, value, null);
|
||||
}
|
||||
|
||||
function createInitializedIteratorResultChunk<T>(
|
||||
@@ -496,12 +508,7 @@ function createInitializedIteratorResultChunk<T>(
|
||||
done: boolean,
|
||||
): InitializedChunk<IteratorResult<T, T>> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(
|
||||
INITIALIZED,
|
||||
{done: done, value: value},
|
||||
null,
|
||||
response,
|
||||
);
|
||||
return new ReactPromise(INITIALIZED, {done: done, value: value}, null);
|
||||
}
|
||||
|
||||
function createInitializedStreamChunk<
|
||||
@@ -514,7 +521,7 @@ function createInitializedStreamChunk<
|
||||
// We use the reason field to stash the controller since we already have that
|
||||
// field. It's a bit of a hack but efficient.
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(INITIALIZED, value, controller, response);
|
||||
return new ReactPromise(INITIALIZED, value, controller);
|
||||
}
|
||||
|
||||
function createResolvedIteratorResultChunk<T>(
|
||||
@@ -526,10 +533,11 @@ function createResolvedIteratorResultChunk<T>(
|
||||
const iteratorResultJSON =
|
||||
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, null, response);
|
||||
return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, response);
|
||||
}
|
||||
|
||||
function resolveIteratorResultChunk<T>(
|
||||
response: Response,
|
||||
chunk: SomeChunk<IteratorResult<T, T>>,
|
||||
value: UninitializedModel,
|
||||
done: boolean,
|
||||
@@ -537,10 +545,11 @@ function resolveIteratorResultChunk<T>(
|
||||
// To reuse code as much code as possible we add the wrapper element as part of the JSON.
|
||||
const iteratorResultJSON =
|
||||
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
|
||||
resolveModelChunk(chunk, iteratorResultJSON);
|
||||
resolveModelChunk(response, chunk, iteratorResultJSON);
|
||||
}
|
||||
|
||||
function resolveModelChunk<T>(
|
||||
response: Response,
|
||||
chunk: SomeChunk<T>,
|
||||
value: UninitializedModel,
|
||||
): void {
|
||||
@@ -557,6 +566,7 @@ function resolveModelChunk<T>(
|
||||
const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
|
||||
resolvedChunk.status = RESOLVED_MODEL;
|
||||
resolvedChunk.value = value;
|
||||
resolvedChunk.reason = response;
|
||||
if (resolveListeners !== null) {
|
||||
// This is unfortunate that we're reading this eagerly if
|
||||
// we already have listeners attached since they might no
|
||||
@@ -602,6 +612,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
initializingHandler = null;
|
||||
|
||||
const resolvedModel = chunk.value;
|
||||
const response = chunk.reason;
|
||||
|
||||
// We go to the BLOCKED state until we've fully resolved this.
|
||||
// We do this before parsing in case we try to initialize the same chunk
|
||||
@@ -616,7 +627,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
}
|
||||
|
||||
try {
|
||||
const value: T = parseModel(chunk._response, resolvedModel);
|
||||
const value: T = parseModel(response, resolvedModel);
|
||||
// Invoke any listeners added while resolving this model. I.e. cyclic
|
||||
// references. This may or may not fully resolve the model depending on
|
||||
// if they were blocked.
|
||||
@@ -679,6 +690,15 @@ export function reportGlobalError(response: Response, error: Error): void {
|
||||
triggerErrorOnChunk(chunk, error);
|
||||
}
|
||||
});
|
||||
if (__DEV__) {
|
||||
const debugChannel = response._debugChannel;
|
||||
if (debugChannel !== undefined) {
|
||||
// If we don't have any more ways of reading data, we don't have to send any
|
||||
// more neither. So we close the writable side.
|
||||
debugChannel('');
|
||||
response._debugChannel = undefined;
|
||||
}
|
||||
}
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
markAllTracksInOrder();
|
||||
flushComponentPerformance(
|
||||
@@ -1052,6 +1072,10 @@ function waitForReference<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use to avoid the microtask resolution in DEV.
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
(fulfill: any).isReactInternalListener = true;
|
||||
}
|
||||
|
||||
function reject(error: mixed): void {
|
||||
if (handler.errored) {
|
||||
@@ -1359,6 +1383,7 @@ function getOutlinedModel<T>(
|
||||
return chunkValue;
|
||||
case PENDING:
|
||||
case BLOCKED:
|
||||
case HALTED:
|
||||
return waitForReference(chunk, parentObject, key, response, map, path);
|
||||
default:
|
||||
// This is an error. Instead of erroring directly, we're going to encode this on
|
||||
@@ -1406,6 +1431,17 @@ function createFormData(
|
||||
return formData;
|
||||
}
|
||||
|
||||
function applyConstructor(
|
||||
response: Response,
|
||||
model: Function,
|
||||
parentObject: Object,
|
||||
key: string,
|
||||
): void {
|
||||
Object.setPrototypeOf(parentObject, model.prototype);
|
||||
// Delete the property. It was just a placeholder.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
|
||||
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
|
||||
return model[Symbol.iterator]();
|
||||
@@ -1462,10 +1498,6 @@ function parseModelString(
|
||||
}
|
||||
case '@': {
|
||||
// Promise
|
||||
if (value.length === 2) {
|
||||
// Infinite promise that never resolves.
|
||||
return new Promise(() => {});
|
||||
}
|
||||
const id = parseInt(value.slice(2), 16);
|
||||
const chunk = getChunk(response, id);
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
@@ -1586,16 +1618,60 @@ function parseModelString(
|
||||
// BigInt
|
||||
return BigInt(value.slice(2));
|
||||
}
|
||||
case 'P': {
|
||||
if (__DEV__) {
|
||||
// In DEV mode we allow debug objects to specify themselves as instances of
|
||||
// another constructor.
|
||||
const ref = value.slice(2);
|
||||
return getOutlinedModel(
|
||||
response,
|
||||
ref,
|
||||
parentObject,
|
||||
key,
|
||||
applyConstructor,
|
||||
);
|
||||
}
|
||||
//Fallthrough
|
||||
}
|
||||
case 'E': {
|
||||
if (__DEV__) {
|
||||
// In DEV mode we allow indirect eval to produce functions for logging.
|
||||
// This should not compile to eval() because then it has local scope access.
|
||||
const code = value.slice(2);
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
return (0, eval)(value.slice(2));
|
||||
return (0, eval)(code);
|
||||
} catch (x) {
|
||||
// We currently use this to express functions so we fail parsing it,
|
||||
// let's just return a blank function as a place holder.
|
||||
if (code.startsWith('(async function')) {
|
||||
const idx = code.indexOf('(', 15);
|
||||
if (idx !== -1) {
|
||||
const name = code.slice(15, idx).trim();
|
||||
// eslint-disable-next-line no-eval
|
||||
return (0, eval)(
|
||||
'({' + JSON.stringify(name) + ':async function(){}})',
|
||||
)[name];
|
||||
}
|
||||
} else if (code.startsWith('(function')) {
|
||||
const idx = code.indexOf('(', 9);
|
||||
if (idx !== -1) {
|
||||
const name = code.slice(9, idx).trim();
|
||||
// eslint-disable-next-line no-eval
|
||||
return (0, eval)(
|
||||
'({' + JSON.stringify(name) + ':function(){}})',
|
||||
)[name];
|
||||
}
|
||||
} else if (code.startsWith('(class')) {
|
||||
const idx = code.indexOf('{', 6);
|
||||
if (idx !== -1) {
|
||||
const name = code.slice(6, idx).trim();
|
||||
// eslint-disable-next-line no-eval
|
||||
return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[
|
||||
name
|
||||
];
|
||||
}
|
||||
}
|
||||
return function () {};
|
||||
}
|
||||
}
|
||||
@@ -1603,17 +1679,21 @@ function parseModelString(
|
||||
}
|
||||
case 'Y': {
|
||||
if (__DEV__) {
|
||||
if (value.length > 2) {
|
||||
const debugChannel = response._debugChannel;
|
||||
if (debugChannel) {
|
||||
const ref = value.slice(2);
|
||||
debugChannel('R:' + ref); // Release this reference immediately
|
||||
}
|
||||
}
|
||||
|
||||
// In DEV mode we encode omitted objects in logs as a getter that throws
|
||||
// so that when you try to access it on the client, you know why that
|
||||
// happened.
|
||||
Object.defineProperty(parentObject, key, {
|
||||
get: function () {
|
||||
// TODO: We should ideally throw here to indicate a difference.
|
||||
return (
|
||||
'This object has been omitted by React in the console log ' +
|
||||
'to avoid sending too much data from the server. Try logging smaller ' +
|
||||
'or more specific objects.'
|
||||
);
|
||||
return OMITTED_PROP_ERROR;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
@@ -1670,9 +1750,10 @@ function ResponseInstance(
|
||||
encodeFormAction: void | EncodeFormActionCallback,
|
||||
nonce: void | string,
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback,
|
||||
replayConsole: boolean,
|
||||
environmentName: void | string,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugChannel: void | DebugChannelCallback, // DEV-only
|
||||
) {
|
||||
const chunks: Map<number, SomeChunk<any>> = new Map();
|
||||
this._bundlerConfig = bundlerConfig;
|
||||
@@ -1727,6 +1808,7 @@ function ResponseInstance(
|
||||
);
|
||||
}
|
||||
this._debugFindSourceMapURL = findSourceMapURL;
|
||||
this._debugChannel = debugChannel;
|
||||
this._replayConsole = replayConsole;
|
||||
this._rootEnvironmentName = rootEnv;
|
||||
}
|
||||
@@ -1742,9 +1824,10 @@ export function createResponse(
|
||||
encodeFormAction: void | EncodeFormActionCallback,
|
||||
nonce: void | string,
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback,
|
||||
replayConsole: boolean,
|
||||
environmentName: void | string,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugChannel: void | DebugChannelCallback, // DEV-only
|
||||
): Response {
|
||||
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
|
||||
return new ResponseInstance(
|
||||
@@ -1758,9 +1841,26 @@ export function createResponse(
|
||||
findSourceMapURL,
|
||||
replayConsole,
|
||||
environmentName,
|
||||
debugChannel,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDebugHalt(response: Response, id: number): void {
|
||||
const chunks = response._chunks;
|
||||
let chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
chunks.set(id, (chunk = createPendingChunk(response)));
|
||||
} else {
|
||||
}
|
||||
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
|
||||
return;
|
||||
}
|
||||
const haltedChunk: HaltedChunk<any> = (chunk: any);
|
||||
haltedChunk.status = HALTED;
|
||||
haltedChunk.value = null;
|
||||
haltedChunk.reason = null;
|
||||
}
|
||||
|
||||
function resolveModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
@@ -1771,7 +1871,7 @@ function resolveModel(
|
||||
if (!chunk) {
|
||||
chunks.set(id, createResolvedModelChunk(response, model));
|
||||
} else {
|
||||
resolveModelChunk(chunk, model);
|
||||
resolveModelChunk(response, chunk, model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1945,7 +2045,7 @@ function startReadableStream<T>(
|
||||
// to synchronous emitting.
|
||||
previousBlockedChunk = null;
|
||||
}
|
||||
resolveModelChunk(chunk, json);
|
||||
resolveModelChunk(response, chunk, json);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -2033,7 +2133,12 @@ function startAsyncIterable<T>(
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
resolveIteratorResultChunk(buffer[nextWriteIndex], value, false);
|
||||
resolveIteratorResultChunk(
|
||||
response,
|
||||
buffer[nextWriteIndex],
|
||||
value,
|
||||
false,
|
||||
);
|
||||
}
|
||||
nextWriteIndex++;
|
||||
},
|
||||
@@ -2046,12 +2151,18 @@ function startAsyncIterable<T>(
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
resolveIteratorResultChunk(buffer[nextWriteIndex], value, true);
|
||||
resolveIteratorResultChunk(
|
||||
response,
|
||||
buffer[nextWriteIndex],
|
||||
value,
|
||||
true,
|
||||
);
|
||||
}
|
||||
nextWriteIndex++;
|
||||
while (nextWriteIndex < buffer.length) {
|
||||
// In generators, any extra reads from the iterator have the value undefined.
|
||||
resolveIteratorResultChunk(
|
||||
response,
|
||||
buffer[nextWriteIndex++],
|
||||
'"$undefined"',
|
||||
true,
|
||||
@@ -2069,32 +2180,33 @@ function startAsyncIterable<T>(
|
||||
}
|
||||
},
|
||||
};
|
||||
const iterable: $AsyncIterable<T, T, void> = {
|
||||
[ASYNC_ITERATOR](): $AsyncIterator<T, T, void> {
|
||||
let nextReadIndex = 0;
|
||||
return createIterator(arg => {
|
||||
if (arg !== undefined) {
|
||||
throw new Error(
|
||||
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
|
||||
|
||||
const iterable: $AsyncIterable<T, T, void> = ({}: any);
|
||||
// $FlowFixMe[cannot-write]
|
||||
iterable[ASYNC_ITERATOR] = (): $AsyncIterator<T, T, void> => {
|
||||
let nextReadIndex = 0;
|
||||
return createIterator(arg => {
|
||||
if (arg !== undefined) {
|
||||
throw new Error(
|
||||
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
|
||||
);
|
||||
}
|
||||
if (nextReadIndex === buffer.length) {
|
||||
if (closed) {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(
|
||||
INITIALIZED,
|
||||
{done: true, value: undefined},
|
||||
null,
|
||||
);
|
||||
}
|
||||
if (nextReadIndex === buffer.length) {
|
||||
if (closed) {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(
|
||||
INITIALIZED,
|
||||
{done: true, value: undefined},
|
||||
null,
|
||||
response,
|
||||
);
|
||||
}
|
||||
buffer[nextReadIndex] =
|
||||
createPendingChunk<IteratorResult<T, T>>(response);
|
||||
}
|
||||
return buffer[nextReadIndex++];
|
||||
});
|
||||
},
|
||||
buffer[nextReadIndex] =
|
||||
createPendingChunk<IteratorResult<T, T>>(response);
|
||||
}
|
||||
return buffer[nextReadIndex++];
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after
|
||||
// reading through the end, but currently we favor code size over this optimization.
|
||||
resolveStream(
|
||||
@@ -2627,9 +2739,15 @@ function initializeFakeStack(
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env);
|
||||
}
|
||||
if (debugInfo.owner != null) {
|
||||
const owner = debugInfo.owner;
|
||||
if (owner != null) {
|
||||
// Initialize any owners not yet initialized.
|
||||
initializeFakeStack(response, debugInfo.owner);
|
||||
initializeFakeStack(response, owner);
|
||||
if (owner.debugLocation === undefined && debugInfo.debugStack != null) {
|
||||
// If we are the child of this owner, then the owner should be the bottom frame
|
||||
// our stack. We can use it as the implied location of the owner.
|
||||
owner.debugLocation = debugInfo.debugStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2816,7 +2934,29 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
|
||||
// $FlowFixMe[cannot-write]
|
||||
ioInfo.end += response._timeOrigin;
|
||||
|
||||
logIOInfo(ioInfo, response._rootEnvironmentName);
|
||||
const env = response._rootEnvironmentName;
|
||||
const promise = ioInfo.value;
|
||||
if (promise) {
|
||||
const thenable: Thenable<mixed> = (promise: any);
|
||||
switch (thenable.status) {
|
||||
case INITIALIZED:
|
||||
logIOInfo(ioInfo, env, thenable.value);
|
||||
break;
|
||||
case ERRORED:
|
||||
logIOInfoErrored(ioInfo, env, thenable.reason);
|
||||
break;
|
||||
default:
|
||||
// If we haven't resolved the Promise yet, wait to log until have so we can include
|
||||
// its data in the log.
|
||||
promise.then(
|
||||
logIOInfo.bind(null, ioInfo, env),
|
||||
logIOInfoErrored.bind(null, ioInfo, env),
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logIOInfo(ioInfo, env, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIOInfo(
|
||||
@@ -2831,7 +2971,7 @@ function resolveIOInfo(
|
||||
chunks.set(id, chunk);
|
||||
initializeModelChunk(chunk);
|
||||
} else {
|
||||
resolveModelChunk(chunk, model);
|
||||
resolveModelChunk(response, chunk, model);
|
||||
if (chunk.status === RESOLVED_MODEL) {
|
||||
initializeModelChunk(chunk);
|
||||
}
|
||||
@@ -3100,13 +3240,55 @@ function flushComponentPerformance(
|
||||
}
|
||||
// $FlowFixMe: Refined.
|
||||
const asyncInfo: ReactAsyncInfo = candidateInfo;
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
response._rootEnvironmentName,
|
||||
);
|
||||
const env = response._rootEnvironmentName;
|
||||
const promise = asyncInfo.awaited.value;
|
||||
if (promise) {
|
||||
const thenable: Thenable<mixed> = (promise: any);
|
||||
switch (thenable.status) {
|
||||
case INITIALIZED:
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
thenable.value,
|
||||
);
|
||||
break;
|
||||
case ERRORED:
|
||||
logComponentAwaitErrored(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
thenable.reason,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// We assume that we should have received the data by now since this is logged at the
|
||||
// end of the response stream. This is more sensitive to ordering so we don't wait
|
||||
// to log it.
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3329,6 +3511,10 @@ function processFullStringRow(
|
||||
}
|
||||
// Fallthrough
|
||||
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
|
||||
if (__DEV__ && row === '') {
|
||||
resolveDebugHalt(response, id);
|
||||
return;
|
||||
}
|
||||
// We assume anything else is JSON.
|
||||
resolveModel(response, id, row);
|
||||
return;
|
||||
|
||||
@@ -17,14 +17,143 @@ import type {
|
||||
|
||||
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
|
||||
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
const supportsUserTiming =
|
||||
enableProfilerTimer &&
|
||||
typeof console !== 'undefined' &&
|
||||
typeof console.timeStamp === 'function';
|
||||
typeof console.timeStamp === 'function' &&
|
||||
typeof performance !== 'undefined' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance.measure === 'function';
|
||||
|
||||
const IO_TRACK = 'Server Requests ⚛';
|
||||
const COMPONENTS_TRACK = 'Server Components ⚛';
|
||||
|
||||
const EMPTY_ARRAY = 0;
|
||||
const COMPLEX_ARRAY = 1;
|
||||
const PRIMITIVE_ARRAY = 2; // Primitive values only
|
||||
const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
|
||||
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
|
||||
let kind = EMPTY_ARRAY;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const value = array[i];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (
|
||||
isArray(value) &&
|
||||
value.length === 2 &&
|
||||
typeof value[0] === 'string'
|
||||
) {
|
||||
// Key value tuple
|
||||
if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
|
||||
return COMPLEX_ARRAY;
|
||||
}
|
||||
kind = ENTRIES_ARRAY;
|
||||
} else {
|
||||
return COMPLEX_ARRAY;
|
||||
}
|
||||
} else if (typeof value === 'function') {
|
||||
return COMPLEX_ARRAY;
|
||||
} else if (typeof value === 'string' && value.length > 50) {
|
||||
return COMPLEX_ARRAY;
|
||||
} else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
|
||||
return COMPLEX_ARRAY;
|
||||
} else {
|
||||
kind = PRIMITIVE_ARRAY;
|
||||
}
|
||||
}
|
||||
return kind;
|
||||
}
|
||||
|
||||
function addObjectToProperties(
|
||||
object: Object,
|
||||
properties: Array<[string, string]>,
|
||||
indent: number,
|
||||
): void {
|
||||
for (const key in object) {
|
||||
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
|
||||
const value = object[key];
|
||||
addValueToProperties(key, value, properties, indent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addValueToProperties(
|
||||
propertyName: string,
|
||||
value: mixed,
|
||||
properties: Array<[string, string]>,
|
||||
indent: number,
|
||||
): void {
|
||||
let desc;
|
||||
switch (typeof value) {
|
||||
case 'object':
|
||||
if (value === null) {
|
||||
desc = 'null';
|
||||
break;
|
||||
} else {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const objectToString = Object.prototype.toString.call(value);
|
||||
let objectName = objectToString.slice(8, objectToString.length - 1);
|
||||
if (objectName === 'Array') {
|
||||
const array: Array<any> = (value: any);
|
||||
const kind = getArrayKind(array);
|
||||
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
|
||||
desc = JSON.stringify(array);
|
||||
break;
|
||||
} else if (kind === ENTRIES_ARRAY) {
|
||||
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const entry = array[i];
|
||||
addValueToProperties(entry[0], entry[1], properties, indent + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (objectName === 'Object') {
|
||||
const proto: any = Object.getPrototypeOf(value);
|
||||
if (proto && typeof proto.constructor === 'function') {
|
||||
objectName = proto.constructor.name;
|
||||
}
|
||||
}
|
||||
properties.push([
|
||||
'\xa0\xa0'.repeat(indent) + propertyName,
|
||||
objectName === 'Object' ? '' : objectName,
|
||||
]);
|
||||
if (indent < 3) {
|
||||
addObjectToProperties(value, properties, indent + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'function':
|
||||
if (value.name === '') {
|
||||
desc = '() => {}';
|
||||
} else {
|
||||
desc = value.name + '() {}';
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
if (value === OMITTED_PROP_ERROR) {
|
||||
desc = '...';
|
||||
} else {
|
||||
desc = JSON.stringify(value);
|
||||
}
|
||||
break;
|
||||
case 'undefined':
|
||||
desc = 'undefined';
|
||||
break;
|
||||
case 'boolean':
|
||||
desc = value ? 'true' : 'false';
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
desc = String(value);
|
||||
}
|
||||
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
|
||||
}
|
||||
|
||||
export function markAllTracksInOrder() {
|
||||
if (supportsUserTiming) {
|
||||
// Ensure we create the Server Component track groups earlier than the Client Scheduler
|
||||
@@ -133,12 +262,7 @@ export function logComponentErrored(
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
if (
|
||||
__DEV__ &&
|
||||
typeof performance !== 'undefined' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance.measure === 'function'
|
||||
) {
|
||||
if (__DEV__) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
@@ -228,12 +352,68 @@ function getIOColor(
|
||||
}
|
||||
}
|
||||
|
||||
export function logComponentAwaitErrored(
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
trackIdx: number,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
rootEnv: string,
|
||||
error: mixed,
|
||||
): void {
|
||||
if (supportsUserTiming && endTime > 0) {
|
||||
const env = asyncInfo.env;
|
||||
const name = asyncInfo.awaited.name;
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
'await ' +
|
||||
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
|
||||
const debugTask = asyncInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
typeof error.message === 'string'
|
||||
? // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error.message)
|
||||
: // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error);
|
||||
const properties = [['Rejected', message]];
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: 'error',
|
||||
track: trackNames[trackIdx],
|
||||
trackGroup: COMPONENTS_TRACK,
|
||||
properties,
|
||||
tooltipText: entryName + ' Rejected',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
trackNames[trackIdx],
|
||||
COMPONENTS_TRACK,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logComponentAwait(
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
trackIdx: number,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
rootEnv: string,
|
||||
value: mixed,
|
||||
): void {
|
||||
if (supportsUserTiming && endTime > 0) {
|
||||
const env = asyncInfo.env;
|
||||
@@ -245,17 +425,26 @@ export function logComponentAwait(
|
||||
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
|
||||
const debugTask = asyncInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const properties: Array<[string, string]> = [];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
addObjectToProperties(value, properties, 0);
|
||||
} else if (value !== undefined) {
|
||||
addValueToProperties('Resolved', value, properties, 0);
|
||||
}
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
trackNames[trackIdx],
|
||||
COMPONENTS_TRACK,
|
||||
color,
|
||||
),
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: color,
|
||||
track: trackNames[trackIdx],
|
||||
trackGroup: COMPONENTS_TRACK,
|
||||
properties,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
@@ -270,7 +459,63 @@ export function logComponentAwait(
|
||||
}
|
||||
}
|
||||
|
||||
export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
|
||||
export function logIOInfoErrored(
|
||||
ioInfo: ReactIOInfo,
|
||||
rootEnv: string,
|
||||
error: mixed,
|
||||
): void {
|
||||
const startTime = ioInfo.start;
|
||||
const endTime = ioInfo.end;
|
||||
if (supportsUserTiming && endTime >= 0) {
|
||||
const name = ioInfo.name;
|
||||
const env = ioInfo.env;
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
const debugTask = ioInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
typeof error.message === 'string'
|
||||
? // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error.message)
|
||||
: // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error);
|
||||
const properties = [['Rejected', message]];
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: 'error',
|
||||
track: IO_TRACK,
|
||||
properties,
|
||||
tooltipText: entryName + ' Rejected',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
undefined,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logIOInfo(
|
||||
ioInfo: ReactIOInfo,
|
||||
rootEnv: string,
|
||||
value: mixed,
|
||||
): void {
|
||||
const startTime = ioInfo.start;
|
||||
const endTime = ioInfo.end;
|
||||
if (supportsUserTiming && endTime >= 0) {
|
||||
@@ -282,17 +527,25 @@ export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
|
||||
const debugTask = ioInfo.debugTask;
|
||||
const color = getIOColor(name);
|
||||
if (__DEV__ && debugTask) {
|
||||
const properties: Array<[string, string]> = [];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
addObjectToProperties(value, properties, 0);
|
||||
} else if (value !== undefined) {
|
||||
addValueToProperties('Resolved', value, properties, 0);
|
||||
}
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
undefined,
|
||||
color,
|
||||
),
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: color,
|
||||
track: IO_TRACK,
|
||||
properties,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
|
||||
13
packages/react-client/src/ReactFlightPropertyAccess.js
vendored
Normal file
13
packages/react-client/src/ReactFlightPropertyAccess.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export const OMITTED_PROP_ERROR =
|
||||
'This object has been omitted by React in the console log ' +
|
||||
'to avoid sending too much data from the server. Try logging smaller ' +
|
||||
'or more specific objects.';
|
||||
@@ -69,7 +69,7 @@ function getErrorForJestMatcher(error) {
|
||||
|
||||
function normalizeComponentInfo(debugInfo) {
|
||||
if (Array.isArray(debugInfo.stack)) {
|
||||
const {debugTask, debugStack, ...copy} = debugInfo;
|
||||
const {debugTask, debugStack, debugLocation, ...copy} = debugInfo;
|
||||
copy.stack = formatV8Stack(debugInfo.stack);
|
||||
if (debugInfo.owner) {
|
||||
copy.owner = normalizeComponentInfo(debugInfo.owner);
|
||||
@@ -3208,12 +3208,29 @@ describe('ReactFlight', () => {
|
||||
return 'hello';
|
||||
}
|
||||
|
||||
class MyClass {
|
||||
constructor() {
|
||||
this.x = 1;
|
||||
}
|
||||
method() {}
|
||||
get y() {
|
||||
return this.x + 1;
|
||||
}
|
||||
get z() {
|
||||
return this.x + 5;
|
||||
}
|
||||
}
|
||||
Object.defineProperty(MyClass.prototype, 'y', {enumerable: true});
|
||||
|
||||
function ServerComponent() {
|
||||
console.log('hi', {
|
||||
prop: 123,
|
||||
fn: foo,
|
||||
map: new Map([['foo', foo]]),
|
||||
promise: new Promise(() => {}),
|
||||
promise: Promise.resolve('yo'),
|
||||
infinitePromise: new Promise(() => {}),
|
||||
Class: MyClass,
|
||||
instance: new MyClass(),
|
||||
});
|
||||
throw new Error('err');
|
||||
}
|
||||
@@ -3258,9 +3275,14 @@ describe('ReactFlight', () => {
|
||||
});
|
||||
ownerStacks = [];
|
||||
|
||||
// Let the Promises resolve.
|
||||
await 0;
|
||||
await 0;
|
||||
await 0;
|
||||
|
||||
// The error should not actually get logged because we're not awaiting the root
|
||||
// so it's not thrown but the server log also shouldn't be replayed.
|
||||
await ReactNoopFlightClient.read(transport);
|
||||
await ReactNoopFlightClient.read(transport, {close: true});
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
|
||||
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
|
||||
@@ -3276,9 +3298,40 @@ describe('ReactFlight', () => {
|
||||
expect(typeof loggedFn2).toBe('function');
|
||||
expect(loggedFn2).not.toBe(foo);
|
||||
expect(loggedFn2.toString()).toBe(foo.toString());
|
||||
expect(loggedFn2).toBe(loggedFn);
|
||||
|
||||
const promise = mockConsoleLog.mock.calls[0][1].promise;
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
expect(await promise).toBe('yo');
|
||||
|
||||
const infinitePromise = mockConsoleLog.mock.calls[0][1].infinitePromise;
|
||||
expect(infinitePromise).toBeInstanceOf(Promise);
|
||||
let resolved = false;
|
||||
infinitePromise.then(
|
||||
() => (resolved = true),
|
||||
x => {
|
||||
console.error(x);
|
||||
resolved = true;
|
||||
},
|
||||
);
|
||||
await 0;
|
||||
await 0;
|
||||
await 0;
|
||||
// This should not reject upon aborting the stream.
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
const Class = mockConsoleLog.mock.calls[0][1].Class;
|
||||
const instance = mockConsoleLog.mock.calls[0][1].instance;
|
||||
expect(typeof Class).toBe('function');
|
||||
expect(Class.prototype.constructor).toBe(Class);
|
||||
expect(instance instanceof Class).toBe(true);
|
||||
expect(Object.getPrototypeOf(instance)).toBe(Class.prototype);
|
||||
expect(instance.x).toBe(1);
|
||||
expect(instance.hasOwnProperty('y')).toBe(true);
|
||||
expect(instance.y).toBe(2); // Enumerable getter was reified
|
||||
expect(instance.hasOwnProperty('z')).toBe(false);
|
||||
expect(instance.z).toBe(6); // Not enumerable getter was transferred as part of the toString() of the class
|
||||
expect(typeof instance.method).toBe('function'); // Methods are included only if they're part of the toString()
|
||||
|
||||
expect(ownerStacks).toEqual(['\n in App (at **)']);
|
||||
});
|
||||
@@ -3327,19 +3380,10 @@ describe('ReactFlight', () => {
|
||||
await ReactNoopFlightClient.read(transport);
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
|
||||
// TODO: Support cyclic objects in console encoding.
|
||||
// expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
|
||||
// const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
|
||||
// expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
|
||||
// expect(cyclic2.cycle).toBe(cyclic2);
|
||||
expect(mockConsoleLog.mock.calls[0][0]).toBe(
|
||||
'Unknown Value: React could not send it from the server.',
|
||||
);
|
||||
expect(mockConsoleLog.mock.calls[0][1].message).toBe(
|
||||
'Converting circular structure to JSON\n' +
|
||||
" --> starting at object with constructor 'Object'\n" +
|
||||
" --- property 'cycle' closes the circle",
|
||||
);
|
||||
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
|
||||
const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
|
||||
expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
|
||||
expect(cyclic2.cycle).toBe(cyclic2);
|
||||
});
|
||||
|
||||
// @gate !__DEV__ || enableComponentPerformanceTrack
|
||||
|
||||
139
packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js
vendored
Normal file
139
packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
if (typeof Blob === 'undefined') {
|
||||
global.Blob = require('buffer').Blob;
|
||||
}
|
||||
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
|
||||
global.File = require('undici').File;
|
||||
global.FormData = require('undici').FormData;
|
||||
}
|
||||
|
||||
function formatV8Stack(stack) {
|
||||
let v8StyleStack = '';
|
||||
if (stack) {
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const [name] = stack[i];
|
||||
if (v8StyleStack !== '') {
|
||||
v8StyleStack += '\n';
|
||||
}
|
||||
v8StyleStack += ' in ' + name + ' (at **)';
|
||||
}
|
||||
}
|
||||
return v8StyleStack;
|
||||
}
|
||||
|
||||
function normalizeComponentInfo(debugInfo) {
|
||||
if (Array.isArray(debugInfo.stack)) {
|
||||
const {debugTask, debugStack, ...copy} = debugInfo;
|
||||
copy.stack = formatV8Stack(debugInfo.stack);
|
||||
if (debugInfo.owner) {
|
||||
copy.owner = normalizeComponentInfo(debugInfo.owner);
|
||||
}
|
||||
return copy;
|
||||
} else {
|
||||
return debugInfo;
|
||||
}
|
||||
}
|
||||
|
||||
function getDebugInfo(obj) {
|
||||
const debugInfo = obj._debugInfo;
|
||||
if (debugInfo) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
return debugInfo;
|
||||
}
|
||||
|
||||
let act;
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let ReactNoopFlightServer;
|
||||
let ReactNoopFlightClient;
|
||||
|
||||
describe('ReactFlight', () => {
|
||||
beforeEach(() => {
|
||||
// Mock performance.now for timing tests
|
||||
let time = 10;
|
||||
const now = jest.fn().mockImplementation(() => {
|
||||
return time++;
|
||||
});
|
||||
Object.defineProperty(performance, 'timeOrigin', {
|
||||
value: time,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(performance, 'now', {
|
||||
value: now,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
jest.resetModules();
|
||||
jest.mock('react', () => require('react/react.react-server'));
|
||||
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
|
||||
// This stores the state so we need to preserve it
|
||||
const flightModules = require('react-noop-renderer/flight-modules');
|
||||
jest.resetModules();
|
||||
__unmockReact();
|
||||
jest.mock('react-noop-renderer/flight-modules', () => flightModules);
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
|
||||
act = require('internal-test-utils').act;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// @gate __DEV__ && enableComponentPerformanceTrack
|
||||
it('can render deep but cut off JSX in debug info', async () => {
|
||||
function createDeepJSX(n) {
|
||||
if (n <= 0) {
|
||||
return null;
|
||||
}
|
||||
return <div>{createDeepJSX(n - 1)}</div>;
|
||||
}
|
||||
|
||||
function ServerComponent(props) {
|
||||
return <div>not using props</div>;
|
||||
}
|
||||
|
||||
const debugChannel = {onMessage(message) {}};
|
||||
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
{
|
||||
root: (
|
||||
<ServerComponent>
|
||||
{createDeepJSX(100) /* deper than objectLimit */}
|
||||
</ServerComponent>
|
||||
),
|
||||
},
|
||||
{debugChannel},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const rootModel = await ReactNoopFlightClient.read(transport, {
|
||||
debugChannel,
|
||||
});
|
||||
const root = rootModel.root;
|
||||
const children = getDebugInfo(root)[1].props.children;
|
||||
expect(children.type).toBe('div');
|
||||
expect(children.props.children.type).toBe('div');
|
||||
ReactNoop.render(root);
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
|
||||
});
|
||||
});
|
||||
@@ -5831,13 +5831,21 @@ export function attach(
|
||||
}
|
||||
|
||||
function getSourceForInstance(instance: DevToolsInstance): Source | null {
|
||||
const unresolvedSource = instance.source;
|
||||
let unresolvedSource = instance.source;
|
||||
if (unresolvedSource === null) {
|
||||
// We don't have any source yet. We can try again later in case an owned child mounts later.
|
||||
// TODO: We won't have any information here if the child is filtered.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (instance.kind === VIRTUAL_INSTANCE) {
|
||||
// We might have found one on the virtual instance.
|
||||
const debugLocation = instance.data.debugLocation;
|
||||
if (debugLocation != null) {
|
||||
unresolvedSource = debugLocation;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have the debug stack (the creation stack of the JSX) for any owned child of this
|
||||
// component, then at the bottom of that stack will be a stack frame that is somewhere within
|
||||
// the component's function body. Typically it would be the callsite of the JSX unless there's
|
||||
|
||||
@@ -6,7 +6,7 @@ export const markShellTime =
|
||||
export const clientRenderBoundary =
|
||||
'$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
|
||||
export const completeBoundary =
|
||||
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
|
||||
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];null!==e.parentNode&&e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};\n$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
|
||||
export const completeBoundaryUpgradeToViewTransitions =
|
||||
'$RV=function(A,g){function k(a,b){var e=a.getAttribute(b);e&&(b=a.style,l.push(a,b.viewTransitionName,b.viewTransitionClass),"auto"!==e&&(b.viewTransitionClass=e),(a=a.getAttribute("vt-name"))||(a="_T_"+K++ +"_"),b.viewTransitionName=a,B=!0)}var B=!1,K=0,l=[];try{var f=document.__reactViewTransition;if(f){f.finished.finally($RV.bind(null,g));return}var m=new Map;for(f=1;f<g.length;f+=2)for(var h=g[f].querySelectorAll("[vt-share]"),d=0;d<h.length;d++){var c=h[d];m.set(c.getAttribute("vt-name"),c)}var u=[];for(h=0;h<g.length;h+=2){var C=g[h],x=C.parentNode;if(x){var v=x.getBoundingClientRect();if(v.left||v.top||v.width||v.height){c=C;for(f=0;c;){if(8===c.nodeType){var r=c.data;if("/$"===r)if(0===f)break;else f--;else"$"!==r&&"$?"!==r&&"$~"!==r&&"$!"!==r||f++}else if(1===c.nodeType){d=c;var D=d.getAttribute("vt-name"),y=m.get(D);k(d,y?"vt-share":"vt-exit");y&&(k(y,"vt-share"),m.set(D,null));var E=d.querySelectorAll("[vt-share]");for(d=0;d<E.length;d++){var F=E[d],G=F.getAttribute("vt-name"),\nH=m.get(G);H&&(k(F,"vt-share"),k(H,"vt-share"),m.set(G,null))}}c=c.nextSibling}for(var I=g[h+1],t=I.firstElementChild;t;)null!==m.get(t.getAttribute("vt-name"))&&k(t,"vt-enter"),t=t.nextElementSibling;c=x;do for(var n=c.firstElementChild;n;){var J=n.getAttribute("vt-update");J&&"none"!==J&&!l.includes(n)&&k(n,"vt-update");n=n.nextElementSibling}while((c=c.parentNode)&&1===c.nodeType&&"none"!==c.getAttribute("vt-update"));u.push.apply(u,I.querySelectorAll(\'img[src]:not([loading="lazy"])\'))}}}if(B){var z=\ndocument.__reactViewTransition=document.startViewTransition({update:function(){A(g);for(var a=[document.documentElement.clientHeight,document.fonts.ready],b={},e=0;e<u.length;b={g:b.g},e++)if(b.g=u[e],!b.g.complete){var p=b.g.getBoundingClientRect();0<p.bottom&&0<p.right&&p.top<window.innerHeight&&p.left<window.innerWidth&&(p=new Promise(function(w){return function(q){w.g.addEventListener("load",q);w.g.addEventListener("error",q)}}(b)),a.push(p))}return Promise.race([Promise.all(a),new Promise(function(w){var q=\nperformance.now();setTimeout(w,2300>q&&2E3<q?2300-q:500)})])},types:[]});z.ready.finally(function(){for(var a=l.length-3;0<=a;a-=3){var b=l[a],e=b.style;e.viewTransitionName=l[a+1];e.viewTransitionClass=l[a+1];""===b.getAttribute("style")&&b.removeAttribute("style")}});z.finished.finally(function(){document.__reactViewTransition===z&&(document.__reactViewTransition=null)});$RB=[];return}}catch(a){}A(g)}.bind(null,$RV);';
|
||||
export const completeBoundaryWithStyles =
|
||||
|
||||
@@ -34,11 +34,16 @@ export function revealCompletedBoundaries(batch) {
|
||||
for (let i = 0; i < batch.length; i += 2) {
|
||||
const suspenseIdNode = batch[i];
|
||||
const contentNode = batch[i + 1];
|
||||
// We can detach the content now.
|
||||
// Completions of boundaries within this contentNode will now find the boundary
|
||||
// in its designated place.
|
||||
contentNode.parentNode.removeChild(contentNode);
|
||||
|
||||
if (contentNode.parentNode === null) {
|
||||
// If the client has failed hydration we may have already deleted the streaming
|
||||
// segments. The server may also have emitted a complete instruction but cancelled
|
||||
// the segment. Regardless we can ignore this case.
|
||||
} else {
|
||||
// We can detach the content now.
|
||||
// Completions of boundaries within this contentNode will now find the boundary
|
||||
// in its designated place.
|
||||
contentNode.parentNode.removeChild(contentNode);
|
||||
}
|
||||
// Clear all the existing children. This is complicated because
|
||||
// there can be embedded Suspense boundaries in the fallback.
|
||||
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
|
||||
|
||||
@@ -171,6 +171,7 @@ export function experimental_renderToHTML(
|
||||
undefined,
|
||||
'Markup',
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const flightResponse = createFlightResponse(
|
||||
null,
|
||||
|
||||
@@ -24,7 +24,7 @@ type Source = Array<Uint8Array>;
|
||||
|
||||
const decoderOptions = {stream: true};
|
||||
|
||||
const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
|
||||
const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
|
||||
createStringDecoder() {
|
||||
return new TextDecoder();
|
||||
},
|
||||
@@ -56,6 +56,8 @@ const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
|
||||
|
||||
type ReadOptions = {|
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
debugChannel?: {onMessage: (message: string) => void},
|
||||
close?: boolean,
|
||||
|};
|
||||
|
||||
function read<T>(source: Source, options: ReadOptions): Thenable<T> {
|
||||
@@ -70,10 +72,16 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
|
||||
options !== undefined ? options.findSourceMapURL : undefined,
|
||||
true,
|
||||
undefined,
|
||||
__DEV__ && options !== undefined && options.debugChannel !== undefined
|
||||
? options.debugChannel.onMessage
|
||||
: undefined,
|
||||
);
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
processBinaryChunk(response, source[i], 0);
|
||||
}
|
||||
if (options !== undefined && options.close) {
|
||||
close(response);
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ type Options = {
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
signal?: AbortSignal,
|
||||
debugChannel?: {onMessage?: (message: string) => void},
|
||||
onError?: (error: mixed) => void,
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
@@ -87,6 +88,7 @@ function render(model: ReactClientValue, options?: Options): Destination {
|
||||
undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
__DEV__ && options && options.debugChannel !== undefined,
|
||||
);
|
||||
const signal = options ? options.signal : undefined;
|
||||
if (signal) {
|
||||
@@ -100,6 +102,11 @@ function render(model: ReactClientValue, options?: Options): Destination {
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (__DEV__ && options && options.debugChannel !== undefined) {
|
||||
options.debugChannel.onMessage = message => {
|
||||
ReactNoopFlightServer.resolveDebugMessage(request, message);
|
||||
};
|
||||
}
|
||||
ReactNoopFlightServer.startWork(request);
|
||||
ReactNoopFlightServer.startFlowing(request, destination);
|
||||
return destination;
|
||||
|
||||
@@ -236,7 +236,7 @@ import {
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
pushTopLevelContextObject,
|
||||
invalidateContextProvider,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {
|
||||
getIsHydrating,
|
||||
enterHydrationState,
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
getUnmaskedContext,
|
||||
hasContextChanged,
|
||||
emptyContextObject,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {readContext, checkIfContextChanged} from './ReactFiberNewContext';
|
||||
import {requestUpdateLane, scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
|
||||
import {
|
||||
@@ -695,7 +695,7 @@ function constructClassInstance(
|
||||
}
|
||||
|
||||
// Cache unmasked context so we can avoid recreating masked context unless necessary.
|
||||
// ReactFiberContext usually updates this cache but can't for newly-created instances.
|
||||
// ReactFiberLegacyContext usually updates this cache but can't for newly-created instances.
|
||||
if (isLegacyContextConsumer) {
|
||||
cacheContext(workInProgress, unmaskedContext, context);
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ import {
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
popContext as popLegacyContext,
|
||||
popTopLevelContextObject as popTopLevelLegacyContextObject,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {popProvider} from './ReactFiberNewContext';
|
||||
import {
|
||||
prepareToHydrateHostInstance,
|
||||
|
||||
@@ -79,7 +79,11 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
|
||||
for (let i = debugInfo.length - 1; i >= 0; i--) {
|
||||
const entry = debugInfo[i];
|
||||
if (typeof entry.name === 'string') {
|
||||
info += describeDebugInfoFrame(entry.name, entry.env);
|
||||
info += describeDebugInfoFrame(
|
||||
entry.name,
|
||||
entry.env,
|
||||
entry.debugLocation,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from './ReactFiberWorkLoop';
|
||||
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
|
||||
import {updateContainerSync} from './ReactFiberReconciler';
|
||||
import {emptyContextObject} from './ReactFiberContext';
|
||||
import {emptyContextObject} from './ReactFiberLegacyContext';
|
||||
import {SyncLane} from './ReactFiberLane';
|
||||
import {
|
||||
ClassComponent,
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
processChildContext,
|
||||
emptyContextObject,
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {createFiberRoot} from './ReactFiberRoot';
|
||||
import {isRootDehydrated} from './ReactFiberShellHydration';
|
||||
import {
|
||||
|
||||
33
packages/react-reconciler/src/ReactFiberStack.js
vendored
33
packages/react-reconciler/src/ReactFiberStack.js
vendored
@@ -27,10 +27,6 @@ function createCursor<T>(defaultValue: T): StackCursor<T> {
|
||||
};
|
||||
}
|
||||
|
||||
function isEmpty(): boolean {
|
||||
return index === -1;
|
||||
}
|
||||
|
||||
function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
|
||||
if (index < 0) {
|
||||
if (__DEV__) {
|
||||
@@ -67,31 +63,4 @@ function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
|
||||
|
||||
cursor.current = value;
|
||||
}
|
||||
|
||||
function checkThatStackIsEmpty() {
|
||||
if (__DEV__) {
|
||||
if (index !== -1) {
|
||||
console.error(
|
||||
'Expected an empty stack. Something was not reset properly.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetStackAfterFatalErrorInDev() {
|
||||
if (__DEV__) {
|
||||
index = -1;
|
||||
valueStack.length = 0;
|
||||
fiberStack.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
createCursor,
|
||||
isEmpty,
|
||||
pop,
|
||||
push,
|
||||
// DEV only:
|
||||
checkThatStackIsEmpty,
|
||||
resetStackAfterFatalErrorInDev,
|
||||
};
|
||||
export {createCursor, pop, push};
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
popContext as popLegacyContext,
|
||||
popTopLevelContextObject as popTopLevelLegacyContextObject,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {popProvider} from './ReactFiberNewContext';
|
||||
import {popCacheProvider} from './ReactFiberCacheComponent';
|
||||
import {transferActualDuration} from './ReactProfilerTimer';
|
||||
|
||||
@@ -357,7 +357,7 @@ import {
|
||||
flushSyncWorkOnLegacyRootsOnly,
|
||||
requestTransitionLane,
|
||||
} from './ReactFiberRootScheduler';
|
||||
import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
|
||||
import {getMaskedContext, getUnmaskedContext} from './ReactFiberLegacyContext';
|
||||
import {logUncaughtError} from './ReactFiberErrorLogger';
|
||||
import {
|
||||
deleteScheduledGesture,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
FindSourceMapURLCallback,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
@@ -43,12 +44,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
|
||||
export type Options = {
|
||||
moduleBaseURL?: string,
|
||||
callServer?: CallServerCallback,
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
};
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createResponseFromOptions(options: void | Options) {
|
||||
return createResponse(
|
||||
options && options.moduleBaseURL ? options.moduleBaseURL : '',
|
||||
@@ -67,6 +87,12 @@ function createResponseFromOptions(options: void | Options) {
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
|
||||
import type {Writable} from 'stream';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
import type {Duplex} from 'stream';
|
||||
|
||||
import {Readable} from 'stream';
|
||||
|
||||
import {
|
||||
@@ -27,6 +29,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -50,6 +54,12 @@ export {
|
||||
registerClientReference,
|
||||
} from '../ReactFlightESMReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -67,7 +77,69 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadable(
|
||||
request: Request,
|
||||
stream: Readable | WebSocket,
|
||||
): void {
|
||||
const stringDecoder = createStringDecoder();
|
||||
let lastWasPartial = false;
|
||||
let stringBuffer = '';
|
||||
function onData(chunk: string | Uint8Array) {
|
||||
if (typeof chunk === 'string') {
|
||||
if (lastWasPartial) {
|
||||
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
|
||||
lastWasPartial = false;
|
||||
}
|
||||
stringBuffer += chunk;
|
||||
} else {
|
||||
const buffer: Uint8Array = (chunk: any);
|
||||
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
|
||||
lastWasPartial = true;
|
||||
}
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
}
|
||||
function onError(error: mixed) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
function onClose() {
|
||||
closeDebugChannel(request);
|
||||
}
|
||||
if (
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.addEventListener === 'function' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.binaryType === 'string'
|
||||
) {
|
||||
const ws: WebSocket = (stream: any);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', event => {
|
||||
// $FlowFixMe
|
||||
onData(event.data);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
// $FlowFixMe
|
||||
onError(event.error);
|
||||
});
|
||||
ws.addEventListener('close', onClose);
|
||||
} else {
|
||||
const readable: Readable = (stream: any);
|
||||
readable.on('data', onData);
|
||||
readable.on('error', onError);
|
||||
readable.on('end', onClose);
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
debugChannel?: Readable | Duplex | WebSocket,
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -86,6 +158,7 @@ function renderToPipeableStream(
|
||||
moduleBasePath: ClientManifest,
|
||||
options?: Options,
|
||||
): PipeableStream {
|
||||
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
moduleBasePath,
|
||||
@@ -95,9 +168,13 @@ function renderToPipeableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannel !== undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
if (debugChannel !== undefined) {
|
||||
startReadingFromDebugChannelReadable(request, debugChannel);
|
||||
}
|
||||
return {
|
||||
pipe<T: Writable>(destination: T): T {
|
||||
if (hasStartedFlowing) {
|
||||
@@ -126,11 +203,12 @@ function renderToPipeableStream(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeWritable(readable: any): Writable {
|
||||
// The current host config expects a Writable so we create
|
||||
// a fake writable for now to push into the Readable.
|
||||
return ({
|
||||
write(chunk) {
|
||||
write(chunk: string | Uint8Array) {
|
||||
return readable.push(chunk);
|
||||
},
|
||||
end() {
|
||||
@@ -184,6 +262,7 @@ function prerenderToNodeStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -287,8 +366,8 @@ function decodeReply<T>(
|
||||
export {
|
||||
renderToPipeableStream,
|
||||
prerenderToNodeStream,
|
||||
decodeReplyFromBusboy,
|
||||
decodeReply,
|
||||
decodeReplyFromBusboy,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
*/
|
||||
|
||||
import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerParcel';
|
||||
|
||||
@@ -76,6 +79,24 @@ export function createServerReference<A: Iterable<any>, T>(
|
||||
);
|
||||
}
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
@@ -104,6 +125,7 @@ function startReadingFromStream(
|
||||
}
|
||||
|
||||
export type Options = {
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
@@ -128,6 +150,12 @@ export function createFromReadableStream<T>(
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
startReadingFromStream(response, stream);
|
||||
return getRoot(response);
|
||||
@@ -152,6 +180,12 @@ export function createFromFetch<T>(
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
promiseForResponse.then(
|
||||
function (r) {
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
|
||||
import {
|
||||
preloadModule,
|
||||
@@ -24,6 +27,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -42,12 +47,19 @@ export {
|
||||
registerServerReference,
|
||||
} from '../ReactFlightParcelReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -57,10 +69,55 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
export function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -70,6 +127,7 @@ export function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -83,6 +141,9 @@ export function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -117,9 +178,6 @@ export function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -144,6 +202,7 @@ export function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
|
||||
import {
|
||||
preloadModule,
|
||||
@@ -26,6 +29,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -47,12 +52,19 @@ export {
|
||||
registerServerReference,
|
||||
} from '../ReactFlightParcelReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -62,10 +74,55 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
export function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -75,6 +132,7 @@ export function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -88,6 +146,9 @@ export function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -122,9 +183,6 @@ export function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -149,6 +207,7 @@ export function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user