mirror of
https://github.com/react/react-native.git
synced 2026-09-28 13:23:09 +08:00
Summary: Pull Request resolved: https://github.com/react/react-native/pull/58605 Use global DOM APIs, supported React Native entry points, and package-relative implementation imports throughout Fantom. Use the renderer-only private interface for the existing public-instance conversion helpers instead of importing Node internals. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D120717572 fbshipit-source-id: 1aae11b7b9434c783d42ca289e2c6edd17aa887c
447 lines
14 KiB
JavaScript
447 lines
14 KiB
JavaScript
/**
|
|
* 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 strict-local
|
|
* @format
|
|
*/
|
|
|
|
/**
|
|
* Runtime helpers for the Fantom REPL (`fantom-cli`). This module runs inside
|
|
* the Hermes runtime hosted by the native tester binary. It:
|
|
*
|
|
* - exposes commonly used modules (`React`, `ReactNative`, `Fantom`) as globals
|
|
* so they can be used at the prompt without an explicit import,
|
|
* - maintains a persistent scope shared across evaluations
|
|
* (`global.$$REPL_SCOPE$$`),
|
|
* - captures uncaught errors (Metro routes module-factory errors to
|
|
* `ErrorUtils` rather than re-throwing),
|
|
* - is configured by the host once on startup via `global.$$ReplConfigure$$`
|
|
* (runtime constants and whether to colorize output), and
|
|
* - defines `global.$$ReplReport$$`, which the host appends to each evaluation
|
|
* to report the result (or error) of the last statement back over stdout.
|
|
*/
|
|
|
|
import type {FantomRuntimeConstants} from '@react-native/fantom/src/Constants';
|
|
|
|
import * as Fantom from '@react-native/fantom';
|
|
import {setConstants} from '@react-native/fantom/src/Constants';
|
|
import * as React from 'react';
|
|
import * as ReactNative from 'react-native';
|
|
|
|
// Persistent scope shared across REPL evaluations. Top-level bindings declared
|
|
// at the prompt are bridged in and out of this object by the host so that they
|
|
// survive across evaluations (see repl/replTransform.js).
|
|
global.$$REPL_SCOPE$$ = global.$$REPL_SCOPE$$ ?? {};
|
|
|
|
// The value of the last evaluated expression (if any) and whether there was one.
|
|
global.$$REPL_RESULT$$ = undefined;
|
|
global.$$REPL_HAS_RESULT$$ = false;
|
|
|
|
// Whether to colorize output with ANSI escape codes. Set by the host based on
|
|
// whether its stdout is a TTY.
|
|
global.$$REPL_COLORS$$ = false;
|
|
|
|
// Convenience globals available at the prompt without importing.
|
|
global.React = React;
|
|
global.ReactNative = ReactNative;
|
|
global.Fantom = Fantom;
|
|
|
|
// Configured by the host once, on startup, with values it computes in Node
|
|
// (e.g. `isOSS`, the host platform). This is how the runtime constants are set
|
|
// dynamically at startup rather than hardcoded.
|
|
global.$$ReplConfigure$$ = (config: {
|
|
colors: boolean,
|
|
constants: FantomRuntimeConstants,
|
|
}): void => {
|
|
global.$$REPL_COLORS$$ = config.colors;
|
|
setConstants(config.constants);
|
|
};
|
|
|
|
// Metro's `__r` runs each entry module inside `ErrorUtils` when it is installed,
|
|
// which reports factory errors via the global handler instead of re-throwing.
|
|
// Capture those so `$$ReplReport$$` can surface them to the host.
|
|
let pendingError: unknown = null;
|
|
const errorUtils = global.ErrorUtils;
|
|
if (errorUtils != null) {
|
|
errorUtils.setGlobalHandler((error: unknown) => {
|
|
pendingError = error;
|
|
});
|
|
}
|
|
|
|
function describeError(error: unknown): string {
|
|
if (error instanceof Error) {
|
|
const message = error.message != null ? error.message : '';
|
|
return `${error.name}: ${message.split('\n')[0]}`;
|
|
}
|
|
return String(error);
|
|
}
|
|
|
|
// Inspector that produces output similar to the Chrome DevTools / Node.js
|
|
// console: nested objects and arrays are expanded (up to a depth limit),
|
|
// strings are quoted, functions/classes/Map/Set get readable tags, circular
|
|
// references are detected, and long collections wrap onto multiple lines.
|
|
|
|
const INSPECT_DEPTH = 2;
|
|
const INLINE_BREAK_LENGTH = 72;
|
|
const MAX_ENTRIES = 100;
|
|
|
|
// ANSI colors roughly matching the Node.js / Chrome DevTools console.
|
|
const COLORS = {
|
|
string: '32',
|
|
number: '33',
|
|
boolean: '33',
|
|
nullish: '90',
|
|
symbol: '32',
|
|
fn: '36',
|
|
special: '36',
|
|
date: '35',
|
|
regexp: '31',
|
|
error: '31',
|
|
};
|
|
|
|
function color(code: string, text: string): string {
|
|
return global.$$REPL_COLORS$$ === true ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
}
|
|
|
|
function quoteString(value: string): string {
|
|
return (
|
|
"'" +
|
|
value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n') +
|
|
"'"
|
|
);
|
|
}
|
|
|
|
function formatKey(key: string): string {
|
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : quoteString(key);
|
|
}
|
|
|
|
// Reads an own property value. Uses property descriptors (rather than `[]`
|
|
// indexing, which Flow can't type on an opaque object, or `Reflect`, which
|
|
// isn't available in Hermes) so a single getter is invoked at a time, keeping
|
|
// the per-property error isolation below working.
|
|
function readProperty(target: interface {}, key: string): unknown {
|
|
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
if (descriptor == null) {
|
|
return undefined;
|
|
}
|
|
const getter = descriptor.get;
|
|
if (getter != null) {
|
|
return getter.call(target);
|
|
}
|
|
return descriptor.value;
|
|
}
|
|
|
|
function getConstructorName(value: interface {}): ?string {
|
|
const proto = Object.getPrototypeOf(value);
|
|
if (proto == null) {
|
|
return null;
|
|
}
|
|
const ctor = proto.constructor;
|
|
return ctor != null && typeof ctor.name === 'string' && ctor.name !== ''
|
|
? ctor.name
|
|
: null;
|
|
}
|
|
|
|
function isClassFunction(value: unknown): boolean {
|
|
try {
|
|
return /^class[\s{]/.test(String(value));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Renders a list of already-formatted entries inline if they are short, or one
|
|
// per line (indented) otherwise.
|
|
function wrapEntries(
|
|
open: string,
|
|
close: string,
|
|
prefix: string,
|
|
entries: Array<string>,
|
|
indent: string,
|
|
): string {
|
|
if (entries.length === 0) {
|
|
return prefix + open + close;
|
|
}
|
|
const oneLine = prefix + open + ' ' + entries.join(', ') + ' ' + close;
|
|
if (oneLine.length <= INLINE_BREAK_LENGTH && !oneLine.includes('\n')) {
|
|
return oneLine;
|
|
}
|
|
const inner = indent + ' ';
|
|
return (
|
|
prefix +
|
|
open +
|
|
'\n' +
|
|
entries.map(entry => inner + entry).join(',\n') +
|
|
'\n' +
|
|
indent +
|
|
close
|
|
);
|
|
}
|
|
|
|
function inspectObject(
|
|
value: interface {},
|
|
depth: number,
|
|
seen: Set<unknown>,
|
|
indent: string,
|
|
): string {
|
|
if (value instanceof Error) {
|
|
return color(
|
|
COLORS.error,
|
|
value.stack != null && value.stack !== ''
|
|
? value.stack
|
|
: `${value.name}: ${value.message}`,
|
|
);
|
|
}
|
|
if (value instanceof RegExp) {
|
|
return color(COLORS.regexp, value.toString());
|
|
}
|
|
if (value instanceof Date) {
|
|
return color(
|
|
COLORS.date,
|
|
Number.isNaN(value.getTime()) ? 'Invalid Date' : value.toISOString(),
|
|
);
|
|
}
|
|
if (seen.has(value)) {
|
|
return color(COLORS.special, '[Circular]');
|
|
}
|
|
if (depth < 0) {
|
|
if (Array.isArray(value)) {
|
|
return color(COLORS.special, '[Array]');
|
|
}
|
|
const name = getConstructorName(value);
|
|
return color(
|
|
COLORS.special,
|
|
name != null && name !== 'Object' ? `[${name}]` : '[Object]',
|
|
);
|
|
}
|
|
|
|
seen.add(value);
|
|
const childDepth = depth - 1;
|
|
const childIndent = indent + ' ';
|
|
let result;
|
|
|
|
if (Array.isArray(value)) {
|
|
const entries = [];
|
|
const limit = Math.min(value.length, MAX_ENTRIES);
|
|
for (let i = 0; i < limit; i++) {
|
|
entries.push(inspect(value[i], childDepth, seen, childIndent));
|
|
}
|
|
if (value.length > MAX_ENTRIES) {
|
|
entries.push(`... ${value.length - MAX_ENTRIES} more item(s)`);
|
|
}
|
|
result = wrapEntries('[', ']', '', entries, indent);
|
|
} else if (value instanceof Map) {
|
|
const entries = [];
|
|
let count = 0;
|
|
for (const [k, v] of value) {
|
|
if (count++ >= MAX_ENTRIES) {
|
|
entries.push(`... ${value.size - MAX_ENTRIES} more item(s)`);
|
|
break;
|
|
}
|
|
entries.push(
|
|
inspect(k, childDepth, seen, childIndent) +
|
|
' => ' +
|
|
inspect(v, childDepth, seen, childIndent),
|
|
);
|
|
}
|
|
result = wrapEntries('{', '}', `Map(${value.size}) `, entries, indent);
|
|
} else if (value instanceof Set) {
|
|
const entries = [];
|
|
let count = 0;
|
|
for (const v of value) {
|
|
if (count++ >= MAX_ENTRIES) {
|
|
entries.push(`... ${value.size - MAX_ENTRIES} more item(s)`);
|
|
break;
|
|
}
|
|
entries.push(inspect(v, childDepth, seen, childIndent));
|
|
}
|
|
result = wrapEntries('{', '}', `Set(${value.size}) `, entries, indent);
|
|
} else {
|
|
const name = getConstructorName(value);
|
|
const prefix = name != null && name !== 'Object' ? name + ' ' : '';
|
|
const keys = Object.keys(value);
|
|
const entries = [];
|
|
const limit = Math.min(keys.length, MAX_ENTRIES);
|
|
for (let i = 0; i < limit; i++) {
|
|
const key = keys[i];
|
|
// Inspecting a property must never leak an error into the next
|
|
// evaluation. Some getters (e.g. TurboModule-backed APIs that aren't
|
|
// registered in the Fantom binary) don't throw synchronously: Metro
|
|
// routes the error through `ErrorUtils` (into `pendingError`) and the
|
|
// getter returns `undefined`. Detect both cases, surface the error in
|
|
// the property's slot, and always restore the captured-error state.
|
|
const errorBefore = pendingError;
|
|
let rendered;
|
|
try {
|
|
const propertyValue = readProperty(value, key);
|
|
if (pendingError !== errorBefore) {
|
|
rendered = color(
|
|
COLORS.error,
|
|
`[Thrown: ${describeError(pendingError)}]`,
|
|
);
|
|
} else {
|
|
rendered = inspect(propertyValue, childDepth, seen, childIndent);
|
|
}
|
|
} catch (error) {
|
|
rendered = color(COLORS.error, `[Thrown: ${describeError(error)}]`);
|
|
}
|
|
pendingError = errorBefore;
|
|
entries.push(formatKey(key) + ': ' + rendered);
|
|
}
|
|
if (keys.length > MAX_ENTRIES) {
|
|
entries.push(`... ${keys.length - MAX_ENTRIES} more item(s)`);
|
|
}
|
|
result = wrapEntries('{', '}', prefix, entries, indent);
|
|
}
|
|
|
|
seen.delete(value);
|
|
return result;
|
|
}
|
|
|
|
function inspect(
|
|
value: unknown,
|
|
depth: number,
|
|
seen: Set<unknown>,
|
|
indent: string,
|
|
): string {
|
|
switch (typeof value) {
|
|
case 'string':
|
|
return color(COLORS.string, quoteString(value));
|
|
case 'number':
|
|
return color(COLORS.number, Object.is(value, -0) ? '-0' : String(value));
|
|
case 'boolean':
|
|
return color(COLORS.boolean, String(value));
|
|
case 'bigint':
|
|
return color(COLORS.number, String(value) + 'n');
|
|
case 'undefined':
|
|
return color(COLORS.nullish, 'undefined');
|
|
case 'symbol':
|
|
return color(COLORS.symbol, value.toString());
|
|
case 'function': {
|
|
const name = typeof value.name === 'string' ? value.name : '';
|
|
if (isClassFunction(value)) {
|
|
return color(
|
|
COLORS.fn,
|
|
name !== '' ? `[class ${name}]` : '[class (anonymous)]',
|
|
);
|
|
}
|
|
return color(
|
|
COLORS.fn,
|
|
name !== '' ? `[Function: ${name}]` : '[Function (anonymous)]',
|
|
);
|
|
}
|
|
case 'object': {
|
|
if (value === null) {
|
|
return color(COLORS.nullish, 'null');
|
|
}
|
|
return inspectObject(value, depth, seen, indent);
|
|
}
|
|
default:
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
function formatValue(value: unknown): string {
|
|
// Inspecting must never change the captured-error state used to report
|
|
// genuine evaluation errors (getters probed here may set it as a side effect).
|
|
const errorBefore = pendingError;
|
|
try {
|
|
return inspect(value, INSPECT_DEPTH, new Set<unknown>(), '');
|
|
} catch {
|
|
return String(value);
|
|
} finally {
|
|
pendingError = errorBefore;
|
|
}
|
|
}
|
|
|
|
function reportJSON(message: interface {}): void {
|
|
const json = JSON.stringify(message);
|
|
if (json == null) {
|
|
return;
|
|
}
|
|
// Force the import of the native module to be lazy.
|
|
const NativeFantom =
|
|
require('../../../packages/react-native/src/private/testing/fantom/specs/NativeFantom').default;
|
|
NativeFantom.reportTestSuiteResultsJSON(json);
|
|
}
|
|
|
|
// Walks the prototype chain collecting own property names (own + inherited),
|
|
// without reading any values (so getters are never invoked).
|
|
function collectPropertyNames(target: unknown): Set<string> {
|
|
const names = new Set<string>();
|
|
let current: unknown = target;
|
|
let depth = 0;
|
|
while (current != null && typeof current === 'object' && depth < 50) {
|
|
for (const name of Object.getOwnPropertyNames(current)) {
|
|
names.add(name);
|
|
}
|
|
current = Object.getPrototypeOf(current);
|
|
depth++;
|
|
}
|
|
return names;
|
|
}
|
|
|
|
// Called by the host to compute autocompletion candidates. `thunk` is either
|
|
// null (complete global identifiers + REPL scope bindings) or a function that
|
|
// returns the object whose properties should be completed.
|
|
global.$$ReplComplete$$ = (thunk: ?() => unknown): void => {
|
|
// Completion must never affect the error state of real evaluations.
|
|
const errorBefore = pendingError;
|
|
const names = new Set<string>();
|
|
try {
|
|
if (thunk == null) {
|
|
for (const name of collectPropertyNames(globalThis)) {
|
|
names.add(name);
|
|
}
|
|
for (const key of Object.keys(global.$$REPL_SCOPE$$)) {
|
|
names.add(key);
|
|
}
|
|
} else {
|
|
const target = thunk();
|
|
if (target != null) {
|
|
for (const name of collectPropertyNames(Object(target))) {
|
|
names.add(name);
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// No completions available (e.g. the expression threw).
|
|
}
|
|
pendingError = errorBefore;
|
|
|
|
const candidates = [];
|
|
for (const name of names) {
|
|
if (!name.startsWith('$$Repl') && !name.startsWith('$$REPL')) {
|
|
candidates.push(name);
|
|
}
|
|
}
|
|
reportJSON({type: 'repl-completions', names: candidates});
|
|
};
|
|
|
|
global.$$ReplReport$$ = (): void => {
|
|
const error = pendingError;
|
|
if (error != null) {
|
|
pendingError = null;
|
|
global.$$REPL_HAS_RESULT$$ = false;
|
|
global.$$REPL_RESULT$$ = undefined;
|
|
reportJSON({
|
|
type: 'repl-error',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error && error.stack != null ? error.stack : '',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (global.$$REPL_HAS_RESULT$$ !== true) {
|
|
return;
|
|
}
|
|
|
|
const text = formatValue(global.$$REPL_RESULT$$);
|
|
global.$$REPL_HAS_RESULT$$ = false;
|
|
global.$$REPL_RESULT$$ = undefined;
|
|
reportJSON({type: 'repl-result', text});
|
|
};
|