Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae3444675f |
11
.github/workflows/compiler_prereleases.yml
vendored
11
.github/workflows/compiler_prereleases.yml
vendored
@@ -19,9 +19,6 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
@@ -58,13 +55,7 @@ jobs:
|
||||
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- if: inputs.dry_run == true
|
||||
name: Publish packages to npm (dry run)
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
- if: inputs.dry_run != true
|
||||
name: Publish packages to npm
|
||||
- name: Publish packages to npm
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
|
||||
@@ -17,9 +17,6 @@ on:
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -36,6 +33,5 @@ jobs:
|
||||
dist_tag: ${{ inputs.dist_tag }}
|
||||
version_name: ${{ inputs.version_name }}
|
||||
tag_version: ${{ inputs.tag_version }}
|
||||
dry_run: ${{ inputs.dry_run }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -19,6 +19,5 @@ jobs:
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
version_name: '0.0.0'
|
||||
dry_run: false
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
4
.github/workflows/runtime_prereleases.yml
vendored
4
.github/workflows/runtime_prereleases.yml
vendored
@@ -82,6 +82,7 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
@@ -90,10 +91,11 @@ jobs:
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
|
||||
@@ -9,7 +9,7 @@ Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2)
|
||||
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
|
||||
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
|
||||
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
|
||||
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools
|
||||
- [React Performance tracks](https://react.dev/reference/developer-tooling/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools
|
||||
|
||||
### New React DOM Features
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '7.0.0',
|
||||
'eslint-plugin-react-hooks': '6.2.0',
|
||||
'jest-react': '0.18.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
|
||||
@@ -314,36 +314,6 @@ test('disableMemoizationForDebugging flag works as expected', async ({
|
||||
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
|
||||
});
|
||||
|
||||
test('error is displayed when source has syntax error', async ({page}) => {
|
||||
const syntaxErrorSource = `function TestComponent(props) {
|
||||
const oops = props.
|
||||
return (
|
||||
<>{oops}</>
|
||||
);
|
||||
}`;
|
||||
const store: Store = {
|
||||
source: syntaxErrorSource,
|
||||
config: defaultConfig,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
await page.goto(`/#${hash}`);
|
||||
await page.waitForFunction(isMonacoLoaded);
|
||||
await expandConfigs(page);
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: 'test-results/08-source-syntax-error.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = text.join('');
|
||||
|
||||
expect(output.replace(/\s+/g, ' ')).toContain(
|
||||
'Expected identifier to be defined before being used',
|
||||
);
|
||||
});
|
||||
|
||||
TEST_CASE_INPUTS.forEach((t, idx) =>
|
||||
test(`playground compiles: ${t.name}`, async ({page}) => {
|
||||
const store: Store = {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import {Resizable} from 're-resizable';
|
||||
import React, {
|
||||
useCallback,
|
||||
useId,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
@@ -22,6 +23,7 @@ export default function AccordionWindow(props: {
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
@@ -35,6 +37,7 @@ export default function AccordionWindow(props: {
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
isFailure={props.isFailure}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -49,6 +52,7 @@ function AccordionWindowItem({
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
isFailure,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
@@ -58,11 +62,11 @@ function AccordionWindowItem({
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
const id = useId();
|
||||
const isShow = tabsOpen.has(name);
|
||||
const isShow = isFailure ? name === 'Output' : tabsOpen.has(name);
|
||||
|
||||
const transitionName = `accordion-window-item-${id}`;
|
||||
|
||||
const toggleTabs = (): void => {
|
||||
const toggleTabs = () => {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
const nextState = new Set(tabsOpen);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
|
||||
import {PluginOptions} from 'babel-plugin-react-compiler';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import React, {
|
||||
@@ -17,8 +18,9 @@ import React, {
|
||||
} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoConfigOptions} from './monacoOptions';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
|
||||
|
||||
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
|
||||
@@ -27,9 +29,9 @@ import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor({
|
||||
formattedAppliedConfig,
|
||||
appliedOptions,
|
||||
}: {
|
||||
formattedAppliedConfig: string;
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): React.ReactElement {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
@@ -47,7 +49,7 @@ export default function ConfigEditor({
|
||||
setIsExpanded(false);
|
||||
});
|
||||
}}
|
||||
formattedAppliedConfig={formattedAppliedConfig}
|
||||
appliedOptions={appliedOptions}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -69,10 +71,10 @@ export default function ConfigEditor({
|
||||
|
||||
function ExpandedEditor({
|
||||
onToggle,
|
||||
formattedAppliedConfig,
|
||||
appliedOptions,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
formattedAppliedConfig: string;
|
||||
onToggle: () => void;
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): React.ReactElement {
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
@@ -120,6 +122,13 @@ function ExpandedEditor({
|
||||
});
|
||||
};
|
||||
|
||||
const formattedAppliedOptions = appliedOptions
|
||||
? prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
})
|
||||
: 'Invalid configs';
|
||||
|
||||
return (
|
||||
<ViewTransition
|
||||
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
|
||||
@@ -149,7 +158,7 @@ function ExpandedEditor({
|
||||
Config Overrides
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 border border-gray-300">
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
@@ -158,7 +167,16 @@ function ExpandedEditor({
|
||||
onChange={handleChange}
|
||||
loading={''}
|
||||
className="monaco-editor-config"
|
||||
options={monacoConfigOptions}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,16 +186,23 @@ function ExpandedEditor({
|
||||
Applied Configs
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 border border-gray-300">
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedConfig}
|
||||
value={formattedAppliedOptions}
|
||||
loading={''}
|
||||
className="monaco-editor-applied-config"
|
||||
options={{
|
||||
...monacoConfigOptions,
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
readOnly: true,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,17 +5,316 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
import {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorCategory,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {useDeferredValue, useMemo, useState} from 'react';
|
||||
import {
|
||||
useDeferredValue,
|
||||
useMemo,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
} from 'react';
|
||||
import {useStore} from '../StoreContext';
|
||||
import ConfigEditor from './ConfigEditor';
|
||||
import Input from './Input';
|
||||
import {CompilerOutput, default as Output} from './Output';
|
||||
import {compile} from '../../lib/compilation';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
default as Output,
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: PluginOptions,
|
||||
): CompilerTransformOutput {
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
[
|
||||
'useFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePaginationFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useRefetchableFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useLazyLoadQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePreloadedQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
function parseOptions(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): PluginOptions {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
|
||||
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
|
||||
// Parse config overrides from config editor
|
||||
let configOverrideOptions: any = {};
|
||||
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
|
||||
if (configOverrides.trim()) {
|
||||
if (configMatch && configMatch[1]) {
|
||||
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
|
||||
configOverrideOptions = new Function(`return (${configString})`)();
|
||||
} else {
|
||||
throw new Error('Invalid override format');
|
||||
}
|
||||
}
|
||||
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedPragmaOptions,
|
||||
...configOverrideOptions,
|
||||
environment: {
|
||||
...parsedPragmaOptions.environment,
|
||||
...configOverrideOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
});
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
results.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
let language: 'flow' | 'typescript';
|
||||
if (source.match(/\@flow/)) {
|
||||
language = 'flow';
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
try {
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} catch (err) {
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Config,
|
||||
reason: `Unexpected failure when transforming configs! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (baseOpts) {
|
||||
try {
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Add logger options to the parsed options
|
||||
const opts = {
|
||||
...baseOpts,
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: `Unexpected failure when transforming input! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
const store = useStore();
|
||||
@@ -28,7 +327,6 @@ export default function Editor(): JSX.Element {
|
||||
() => compile(deferredStore.source, 'linter', deferredStore.config),
|
||||
[deferredStore.source, deferredStore.config],
|
||||
);
|
||||
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
|
||||
|
||||
let mergedOutput: CompilerOutput;
|
||||
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
|
||||
@@ -42,22 +340,11 @@ export default function Editor(): JSX.Element {
|
||||
mergedOutput = compilerOutput;
|
||||
errors = compilerOutput.error.details;
|
||||
}
|
||||
|
||||
if (appliedOptions) {
|
||||
const formatted = prettyFormat(appliedOptions, {
|
||||
printFunctionName: false,
|
||||
printBasicPrototype: false,
|
||||
});
|
||||
if (formatted !== formattedAppliedConfig) {
|
||||
setFormattedAppliedConfig(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex top-14">
|
||||
<div className="flex-shrink-0">
|
||||
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
|
||||
<ConfigEditor appliedOptions={appliedOptions} />
|
||||
</div>
|
||||
<div className="flex flex-1 min-w-0">
|
||||
<Input language={language} errors={errors} />
|
||||
|
||||
@@ -27,8 +27,6 @@ import {
|
||||
useState,
|
||||
Suspense,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
@@ -37,7 +35,6 @@ import {BabelFileResult} from '@babel/core';
|
||||
import {
|
||||
CONFIG_PANEL_TRANSITION,
|
||||
TOGGLE_INTERNALS_TRANSITION,
|
||||
EXPAND_ACCORDION_TRANSITION,
|
||||
} from '../../lib/transitionTypes';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
@@ -268,22 +265,8 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
* Update the active tab back to the output or errors tab when the compilation state
|
||||
* changes between success/failure.
|
||||
*/
|
||||
const [previousOutputKind, setPreviousOutputKind] = useState(
|
||||
compilerOutput.kind,
|
||||
);
|
||||
|
||||
const isFailure = compilerOutput.kind !== 'ok';
|
||||
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
if (isFailure) {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
setTabsOpen(prev => new Set(prev).add('Output'));
|
||||
setActiveTab('Output');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
|
||||
let lastResult: string = '';
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
@@ -312,6 +295,8 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
// Display the Output tab on compilation failure
|
||||
activeTabOverride={isFailure ? 'Output' : undefined}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
@@ -330,6 +315,7 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
isFailure={isFailure}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
@@ -386,18 +372,12 @@ function TextTabContent({
|
||||
loading={''}
|
||||
options={{
|
||||
...monacoOptions,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
},
|
||||
dimension: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
readOnly: true,
|
||||
lineNumbers: 'off',
|
||||
glyphMargin: false,
|
||||
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
|
||||
overviewRulerLanes: 0,
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -32,14 +32,3 @@ export const monacoOptions: Partial<EditorProps['options']> = {
|
||||
|
||||
tabSize: 2,
|
||||
};
|
||||
|
||||
export const monacoConfigOptions: Partial<EditorProps['options']> = {
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
};
|
||||
|
||||
@@ -17,11 +17,15 @@ export default function TabbedWindow({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
activeTabOverride,
|
||||
}: {
|
||||
tabs: Map<string, React.ReactNode>;
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
activeTabOverride?: string;
|
||||
}): React.ReactElement {
|
||||
const currentActiveTab = activeTabOverride ? activeTabOverride : activeTab;
|
||||
|
||||
const id = useId();
|
||||
const transitionName = `tab-highlight-${id}`;
|
||||
|
||||
@@ -37,7 +41,7 @@ export default function TabbedWindow({
|
||||
<div className="flex flex-col h-full max-w-full">
|
||||
<div className="flex p-2 flex-shrink-0">
|
||||
{Array.from(tabs.keys()).map(tab => {
|
||||
const isActive = activeTab === tab;
|
||||
const isActive = currentActiveTab === tab;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
@@ -73,7 +77,7 @@ export default function TabbedWindow({
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(activeTab)}
|
||||
{tabs.get(currentActiveTab)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,308 +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 {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import * as t from '@babel/types';
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
CompilerDiagnostic,
|
||||
Effect,
|
||||
ErrorCategory,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
type Hook,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import type {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
PrintedCompilerPipelineValue,
|
||||
} from '../components/Editor/Output';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
options: PluginOptions,
|
||||
): CompilerTransformOutput {
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
[
|
||||
'useFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePaginationFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useRefetchableFragment',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'useLazyLoadQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'usePreloadedQuery',
|
||||
{
|
||||
valueKind: ValueKind.Frozen,
|
||||
effectKind: Effect.Freeze,
|
||||
noAlias: true,
|
||||
transitiveMixedData: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
function parseOptions(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): PluginOptions {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
|
||||
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
environment:
|
||||
mode === 'linter'
|
||||
? {
|
||||
// enabled in compiler
|
||||
validateRefAccessDuringRender: false,
|
||||
// enabled in linter
|
||||
validateNoSetStateInRender: true,
|
||||
validateNoSetStateInEffects: true,
|
||||
validateNoJSXInTryStatements: true,
|
||||
validateNoImpureFunctionsInRender: true,
|
||||
validateStaticComponents: true,
|
||||
validateNoFreezingKnownMutableFunctions: true,
|
||||
validateNoVoidUseMemo: true,
|
||||
}
|
||||
: {
|
||||
/* use defaults for compiler mode */
|
||||
},
|
||||
});
|
||||
|
||||
// Parse config overrides from config editor
|
||||
let configOverrideOptions: any = {};
|
||||
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
|
||||
if (configOverrides.trim()) {
|
||||
if (configMatch && configMatch[1]) {
|
||||
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
|
||||
configOverrideOptions = new Function(`return (${configString})`)();
|
||||
} else {
|
||||
throw new Error('Invalid override format');
|
||||
}
|
||||
}
|
||||
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedPragmaOptions,
|
||||
...configOverrideOptions,
|
||||
environment: {
|
||||
...parsedPragmaOptions.environment,
|
||||
...configOverrideOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
});
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
export function compile(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
configOverrides: string,
|
||||
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
results.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
let language: 'flow' | 'typescript';
|
||||
if (source.match(/\@flow/)) {
|
||||
language = 'flow';
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let transformOutput;
|
||||
|
||||
let baseOpts: PluginOptions | null = null;
|
||||
try {
|
||||
baseOpts = parseOptions(source, mode, configOverrides);
|
||||
} catch (err) {
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Config,
|
||||
reason: `Unexpected failure when transforming configs! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (baseOpts) {
|
||||
try {
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Add logger options to the parsed options
|
||||
const opts = {
|
||||
...baseOpts,
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(event.detail);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.merge(err);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Invariant,
|
||||
reason: `Unexpected failure when transforming input! \n${err}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.details.push(e));
|
||||
}
|
||||
if (error.hasErrors() || !transformOutput) {
|
||||
return [{kind: 'err', results, error}, language, baseOpts];
|
||||
}
|
||||
return [
|
||||
{kind: 'ok', results, transformOutput, errors: error.details},
|
||||
language,
|
||||
baseOpts,
|
||||
];
|
||||
}
|
||||
@@ -124,11 +124,3 @@
|
||||
::view-transition-group(.expand-accordion) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
/**
|
||||
* For some reason, the original Monaco editor is still visible to the
|
||||
* left of the DiffEditor. This is a workaround for better visual clarity.
|
||||
*/
|
||||
.monaco-diff-editor .editor.original{
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^2.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"./**/@babel/parser": "7.7.4",
|
||||
|
||||
@@ -536,8 +536,7 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.StaticComponents:
|
||||
case ErrorCategory.Suppression:
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo:
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
case ErrorCategory.UseMemo: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
@@ -583,10 +582,6 @@ export enum ErrorCategory {
|
||||
* Checking for valid usage of manual memoization
|
||||
*/
|
||||
UseMemo = 'UseMemo',
|
||||
/**
|
||||
* Checking that useMemos always return a value
|
||||
*/
|
||||
VoidUseMemo = 'VoidUseMemo',
|
||||
/**
|
||||
* Checking for higher order functions acting as factories for components/hooks
|
||||
*/
|
||||
@@ -674,21 +669,6 @@ export enum ErrorCategory {
|
||||
FBT = 'FBT',
|
||||
}
|
||||
|
||||
export enum LintRulePreset {
|
||||
/**
|
||||
* Rules that are stable and included in the `recommended` preset.
|
||||
*/
|
||||
Recommended = 'recommended',
|
||||
/**
|
||||
* Rules that are more experimental and only included in the `recommended-latest` preset.
|
||||
*/
|
||||
RecommendedLatest = 'recommended-latest',
|
||||
/**
|
||||
* Rules that are disabled.
|
||||
*/
|
||||
Off = 'off',
|
||||
}
|
||||
|
||||
export type LintRule = {
|
||||
// Stores the category the rule corresponds to, used to filter errors when reporting
|
||||
category: ErrorCategory;
|
||||
@@ -709,14 +689,15 @@ export type LintRule = {
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
|
||||
* any preset.
|
||||
* If true, this rule will automatically appear in the default, "recommended" ESLint
|
||||
* rule set. Otherwise it will be part of an `allRules` export that developers can
|
||||
* use to opt-in to showing output of all possible rules.
|
||||
*
|
||||
* NOTE: not all validations are enabled by default! Setting this flag only affects
|
||||
* whether a given rule is part of the recommended set. The corresponding validation
|
||||
* also should be enabled by default if you want the error to actually show up!
|
||||
*/
|
||||
preset: LintRulePreset;
|
||||
recommended: boolean;
|
||||
};
|
||||
|
||||
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
|
||||
@@ -739,7 +720,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
@@ -749,7 +730,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'capitalized-calls',
|
||||
description:
|
||||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Config: {
|
||||
@@ -758,7 +739,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'config',
|
||||
description: 'Validates the compiler configuration options',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDependencies: {
|
||||
@@ -767,7 +748,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memoized-effect-dependencies',
|
||||
description: 'Validates that effect dependencies are memoized',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
@@ -777,7 +758,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates against deriving values from state in an effect',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectSetState: {
|
||||
@@ -787,7 +768,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'set-state-in-effect',
|
||||
description:
|
||||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.ErrorBoundaries: {
|
||||
@@ -797,7 +778,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'error-boundaries',
|
||||
description:
|
||||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Factories: {
|
||||
@@ -808,7 +789,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
description:
|
||||
'Validates against higher order functions defining nested components or hooks. ' +
|
||||
'Components and hooks should be defined at the module level',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
@@ -817,7 +798,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fbt',
|
||||
description: 'Validates usage of fbt',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
@@ -826,7 +807,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
@@ -836,7 +817,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'gating',
|
||||
description:
|
||||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Globals: {
|
||||
@@ -847,7 +828,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
description:
|
||||
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
|
||||
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Hooks: {
|
||||
@@ -861,7 +842,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
|
||||
* this rule.
|
||||
*/
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Immutability: {
|
||||
@@ -871,7 +852,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'immutability',
|
||||
description:
|
||||
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Invariant: {
|
||||
@@ -880,7 +861,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'invariant',
|
||||
description: 'Internal invariants',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.PreserveManualMemo: {
|
||||
@@ -892,7 +873,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
'Validates that existing manual memoized is preserved by the compiler. ' +
|
||||
'React Compiler will only compile components and hooks if its inference ' +
|
||||
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Purity: {
|
||||
@@ -902,7 +883,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'purity',
|
||||
description:
|
||||
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Refs: {
|
||||
@@ -912,7 +893,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'refs',
|
||||
description:
|
||||
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.RenderSetState: {
|
||||
@@ -922,7 +903,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'set-state-in-render',
|
||||
description:
|
||||
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.StaticComponents: {
|
||||
@@ -932,7 +913,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'static-components',
|
||||
description:
|
||||
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Suppression: {
|
||||
@@ -941,7 +922,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'rule-suppression',
|
||||
description: 'Validates against suppression of other rules',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Syntax: {
|
||||
@@ -950,7 +931,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'syntax',
|
||||
description: 'Validates against invalid syntax',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Todo: {
|
||||
@@ -959,7 +940,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Hint,
|
||||
name: 'todo',
|
||||
description: 'Unimplemented features',
|
||||
preset: LintRulePreset.Off,
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UnsupportedSyntax: {
|
||||
@@ -969,7 +950,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'unsupported-syntax',
|
||||
description:
|
||||
'Validates against syntax that we do not plan to support in React Compiler',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.UseMemo: {
|
||||
@@ -979,17 +960,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'use-memo',
|
||||
description:
|
||||
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'void-use-memo',
|
||||
description:
|
||||
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
@@ -999,7 +970,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
name: 'incompatible-library',
|
||||
description:
|
||||
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
||||
preset: LintRulePreset.Recommended,
|
||||
recommended: true,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {z} from 'zod/v4';
|
||||
import {z} from 'zod';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
tryParseExternalFunction,
|
||||
} from '../HIR/Environment';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerPipelineValue} from './Pipeline';
|
||||
|
||||
const PanicThresholdOptionsSchema = z.enum([
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
@@ -83,11 +83,21 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
|
||||
export const USE_FIRE_FUNCTION_NAME = 'useFire';
|
||||
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
|
||||
|
||||
export const MacroSchema = z.string();
|
||||
export const MacroMethodSchema = z.union([
|
||||
z.object({type: z.literal('wildcard')}),
|
||||
z.object({type: z.literal('name'), name: z.string()}),
|
||||
]);
|
||||
|
||||
// Would like to change this to drop the string option, but breaks compatibility with existing configs
|
||||
export const MacroSchema = z.union([
|
||||
z.string(),
|
||||
z.tuple([z.string(), z.array(MacroMethodSchema)]),
|
||||
]);
|
||||
|
||||
export type CompilerMode = 'all_features' | 'no_inferred_memo';
|
||||
|
||||
export type Macro = z.infer<typeof MacroSchema>;
|
||||
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
|
||||
|
||||
const HookSchema = z.object({
|
||||
/*
|
||||
@@ -149,7 +159,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* A function that, given the name of a module, can optionally return a description
|
||||
* of that module's type signature.
|
||||
*/
|
||||
moduleTypeProvider: z.nullable(z.any()).default(null),
|
||||
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* A list of functions which the application compiles as macros, where
|
||||
@@ -239,7 +249,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Allows specifying a function that can populate HIR with type information from
|
||||
* Flow
|
||||
*/
|
||||
flowTypeProvider: z.nullable(z.any()).default(null),
|
||||
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
@@ -649,7 +659,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Invalid:
|
||||
* useMemo(() => { ... }, [...]);
|
||||
*/
|
||||
validateNoVoidUseMemo: z.boolean().default(true),
|
||||
validateNoVoidUseMemo: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope
|
||||
@@ -896,12 +906,6 @@ export class Environment {
|
||||
if (moduleTypeProvider == null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof moduleTypeProvider !== 'function') {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Expected a function for \`moduleTypeProvider\``,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const unparsedModuleConfig = moduleTypeProvider(moduleName);
|
||||
if (unparsedModuleConfig != null) {
|
||||
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod/v4';
|
||||
import {z} from 'zod';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {isReservedWord} from '../Utils/Keyword';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {z} from 'zod/v4';
|
||||
import {z} from 'zod';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {
|
||||
EffectSchema,
|
||||
|
||||
@@ -438,6 +438,40 @@ export function dropManualMemoization(
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bailout on void return useMemos. This is an anti-pattern where code might be using
|
||||
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
|
||||
* values.
|
||||
*/
|
||||
if (
|
||||
func.env.config.validateNoVoidUseMemo &&
|
||||
manualMemo.kind === 'useMemo'
|
||||
) {
|
||||
const funcToCheck = sidemap.functions.get(
|
||||
fnPlace.identifier.id,
|
||||
)?.value;
|
||||
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This ${
|
||||
manualMemo.loadInstr.value.kind === 'PropertyLoad'
|
||||
? 'React.useMemo'
|
||||
: 'useMemo'
|
||||
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instr.value = getManualMemoizationReplacement(
|
||||
fnPlace,
|
||||
instr.value.loc,
|
||||
@@ -595,3 +629,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
getHookKind,
|
||||
HIRFunction,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
@@ -199,7 +198,6 @@ export function inferMutationAliasingEffects(
|
||||
isFunctionExpression,
|
||||
fn,
|
||||
hoistedContextDeclarations,
|
||||
findNonMutatedDestructureSpreads(fn),
|
||||
);
|
||||
|
||||
let iterationCount = 0;
|
||||
@@ -289,18 +287,15 @@ class Context {
|
||||
isFuctionExpression: boolean;
|
||||
fn: HIRFunction;
|
||||
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
|
||||
nonMutatingSpreads: Set<IdentifierId>;
|
||||
|
||||
constructor(
|
||||
isFunctionExpression: boolean,
|
||||
fn: HIRFunction,
|
||||
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
|
||||
nonMutatingSpreads: Set<IdentifierId>,
|
||||
) {
|
||||
this.isFuctionExpression = isFunctionExpression;
|
||||
this.fn = fn;
|
||||
this.hoistedContextDeclarations = hoistedContextDeclarations;
|
||||
this.nonMutatingSpreads = nonMutatingSpreads;
|
||||
}
|
||||
|
||||
cacheApplySignature(
|
||||
@@ -327,161 +322,6 @@ class Context {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds objects created via ObjectPattern spread destructuring
|
||||
* (`const {x, ...spread} = ...`) where a) the rvalue is known frozen and
|
||||
* b) the spread value cannot possibly be directly mutated. The idea is that
|
||||
* for this set of values, we can treat the spread object as frozen.
|
||||
*
|
||||
* The primary use case for this is props spreading:
|
||||
*
|
||||
* ```
|
||||
* function Component({prop, ...otherProps}) {
|
||||
* const transformedProp = transform(prop, otherProps.foo);
|
||||
* // pass `otherProps` down:
|
||||
* return <Foo {...otherProps} prop={transformedProp} />;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Here we know that since `otherProps` cannot be mutated, we don't have to treat
|
||||
* it as mutable: `otherProps.foo` only reads a value that must be frozen, so it
|
||||
* can be treated as frozen too.
|
||||
*/
|
||||
function findNonMutatedDestructureSpreads(fn: HIRFunction): Set<IdentifierId> {
|
||||
const knownFrozen = new Set<IdentifierId>();
|
||||
if (fn.fnType === 'Component') {
|
||||
const [props] = fn.params;
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
knownFrozen.add(props.identifier.id);
|
||||
}
|
||||
} else {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
knownFrozen.add(param.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map of temporaries to identifiers for spread objects
|
||||
const candidateNonMutatingSpreads = new Map<IdentifierId, IdentifierId>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
if (candidateNonMutatingSpreads.size !== 0) {
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands.values()) {
|
||||
const spread = candidateNonMutatingSpreads.get(operand.identifier.id);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.delete(spread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'Destructure': {
|
||||
if (
|
||||
!knownFrozen.has(value.value.identifier.id) ||
|
||||
!(
|
||||
value.lvalue.kind === InstructionKind.Let ||
|
||||
value.lvalue.kind === InstructionKind.Const
|
||||
) ||
|
||||
value.lvalue.pattern.kind !== 'ObjectPattern'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const item of value.lvalue.pattern.properties) {
|
||||
if (item.kind !== 'Spread') {
|
||||
continue;
|
||||
}
|
||||
candidateNonMutatingSpreads.set(
|
||||
item.place.identifier.id,
|
||||
item.place.identifier.id,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
value.place.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
value.value.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
|
||||
candidateNonMutatingSpreads.set(
|
||||
value.lvalue.place.identifier.id,
|
||||
spread,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxFragment':
|
||||
case 'JsxExpression': {
|
||||
// Passing objects created with spread to jsx can't mutate them
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
// Properties must be frozen since the original value was frozen
|
||||
break;
|
||||
}
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (getHookKind(fn.env, callee.identifier) != null) {
|
||||
// Hook calls have frozen arguments, and non-ref returns are frozen
|
||||
if (!isRefOrRefValue(lvalue.identifier)) {
|
||||
knownFrozen.add(lvalue.identifier.id);
|
||||
}
|
||||
} else {
|
||||
// Non-hook calls check their operands, since they are potentially mutable
|
||||
if (candidateNonMutatingSpreads.size !== 0) {
|
||||
// Otherwise any reference to the spread object itself may mutate
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.delete(spread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (candidateNonMutatingSpreads.size !== 0) {
|
||||
// Otherwise any reference to the spread object itself may mutate
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const spread = candidateNonMutatingSpreads.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
if (spread != null) {
|
||||
candidateNonMutatingSpreads.delete(spread);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nonMutatingSpreads = new Set<IdentifierId>();
|
||||
for (const [key, value] of candidateNonMutatingSpreads) {
|
||||
if (key === value) {
|
||||
nonMutatingSpreads.add(key);
|
||||
}
|
||||
}
|
||||
return nonMutatingSpreads;
|
||||
}
|
||||
|
||||
function inferParam(
|
||||
param: Place | SpreadPattern,
|
||||
initialState: InferenceState,
|
||||
@@ -2214,9 +2054,7 @@ function computeSignatureForInstruction(
|
||||
kind: 'Create',
|
||||
into: place,
|
||||
reason: ValueReason.Other,
|
||||
value: context.nonMutatingSpreads.has(place.identifier.id)
|
||||
? ValueKind.Frozen
|
||||
: ValueKind.Mutable,
|
||||
value: ValueKind.Mutable,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
|
||||
@@ -8,41 +8,13 @@
|
||||
import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
makeInstructionId,
|
||||
MutableRange,
|
||||
Place,
|
||||
ReactiveScope,
|
||||
ReactiveValue,
|
||||
} from '../HIR';
|
||||
import {Macro} from '../HIR/Environment';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Whether a macro requires its arguments to be transitively inlined (eg fbt)
|
||||
* or just avoid having the top-level values be converted to variables (eg fbt.param)
|
||||
*/
|
||||
enum InlineLevel {
|
||||
Transitive = 'Transitive',
|
||||
Shallow = 'Shallow',
|
||||
}
|
||||
type MacroDefinition = {
|
||||
level: InlineLevel;
|
||||
properties: Map<string, MacroDefinition> | null;
|
||||
};
|
||||
|
||||
const SHALLOW_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Shallow,
|
||||
properties: null,
|
||||
};
|
||||
const TRANSITIVE_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Transitive,
|
||||
properties: null,
|
||||
};
|
||||
const FBT_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Transitive,
|
||||
properties: new Map([['*', SHALLOW_MACRO]]),
|
||||
};
|
||||
FBT_MACRO.properties!.set('enum', FBT_MACRO);
|
||||
import {Macro, MacroMethod} from '../HIR/Environment';
|
||||
import {eachReactiveValueOperand} from './visitors';
|
||||
|
||||
/**
|
||||
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
|
||||
@@ -67,210 +39,230 @@ FBT_MACRO.properties!.set('enum', FBT_MACRO);
|
||||
* ## User-defined macro-like function
|
||||
*
|
||||
* Users can also specify their own functions to be treated similarly to fbt via the
|
||||
* `customMacros` environment configuration. By default, user-supplied custom macros
|
||||
* have their arguments transitively inlined.
|
||||
* `customMacros` environment configuration.
|
||||
*/
|
||||
export function memoizeFbtAndMacroOperandsInSameScope(
|
||||
fn: HIRFunction,
|
||||
): Set<IdentifierId> {
|
||||
const macroKinds = new Map<Macro, MacroDefinition>([
|
||||
...Array.from(FBT_TAGS.entries()),
|
||||
...(fn.env.config.customMacros ?? []).map(
|
||||
name => [name, TRANSITIVE_MACRO] as [Macro, MacroDefinition],
|
||||
),
|
||||
const fbtMacroTags = new Set<Macro>([
|
||||
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
|
||||
...(fn.env.config.customMacros ?? []),
|
||||
]);
|
||||
/**
|
||||
* Forward data-flow analysis to identify all macro tags, including
|
||||
* things like `fbt.foo.bar(...)`
|
||||
*/
|
||||
const macroTags = populateMacroTags(fn, macroKinds);
|
||||
|
||||
/**
|
||||
* Reverse data-flow analysis to merge arguments to macro *invocations*
|
||||
* based on the kind of the macro
|
||||
*/
|
||||
const macroValues = mergeMacroArguments(fn, macroTags, macroKinds);
|
||||
|
||||
return macroValues;
|
||||
const fbtValues: Set<IdentifierId> = new Set();
|
||||
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
|
||||
while (true) {
|
||||
let vsize = fbtValues.size;
|
||||
let msize = macroMethods.size;
|
||||
visit(fn, fbtMacroTags, fbtValues, macroMethods);
|
||||
if (vsize === fbtValues.size && msize === macroMethods.size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fbtValues;
|
||||
}
|
||||
|
||||
const FBT_TAGS: Map<string, MacroDefinition> = new Map([
|
||||
['fbt', FBT_MACRO],
|
||||
['fbt:param', SHALLOW_MACRO],
|
||||
['fbt:enum', FBT_MACRO],
|
||||
['fbt:plural', SHALLOW_MACRO],
|
||||
['fbs', FBT_MACRO],
|
||||
['fbs:param', SHALLOW_MACRO],
|
||||
['fbs:enum', FBT_MACRO],
|
||||
['fbs:plural', SHALLOW_MACRO],
|
||||
export const FBT_TAGS: Set<string> = new Set([
|
||||
'fbt',
|
||||
'fbt:param',
|
||||
'fbs',
|
||||
'fbs:param',
|
||||
]);
|
||||
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||
'fbt:param',
|
||||
'fbs:param',
|
||||
]);
|
||||
|
||||
function populateMacroTags(
|
||||
function visit(
|
||||
fn: HIRFunction,
|
||||
macroKinds: Map<Macro, MacroDefinition>,
|
||||
): Map<IdentifierId, MacroDefinition> {
|
||||
const macroTags = new Map<IdentifierId, MacroDefinition>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'Primitive': {
|
||||
if (typeof value.value === 'string') {
|
||||
const macroDefinition = macroKinds.get(value.value);
|
||||
if (macroDefinition != null) {
|
||||
/*
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
* all `fbt` string literals in case they are used as a jsx tag.
|
||||
*/
|
||||
macroTags.set(lvalue.identifier.id, macroDefinition);
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instruction of block.instructions) {
|
||||
const {lvalue, value} = instruction;
|
||||
if (lvalue === null) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
value.kind === 'Primitive' &&
|
||||
typeof value.value === 'string' &&
|
||||
matchesExactTag(value.value, fbtMacroTags)
|
||||
) {
|
||||
/*
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
* all `fbt` string literals in case they are used as a jsx tag.
|
||||
*/
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchesExactTag(value.binding.name, fbtMacroTags)
|
||||
) {
|
||||
// Record references to `fbt` as a global
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchTagRoot(value.binding.name, fbtMacroTags) !== null
|
||||
) {
|
||||
const methods = matchTagRoot(value.binding.name, fbtMacroTags)!;
|
||||
macroMethods.set(lvalue.identifier.id, methods);
|
||||
} else if (
|
||||
value.kind === 'PropertyLoad' &&
|
||||
macroMethods.has(value.object.identifier.id)
|
||||
) {
|
||||
const methods = macroMethods.get(value.object.identifier.id)!;
|
||||
const newMethods = [];
|
||||
for (const method of methods) {
|
||||
if (
|
||||
method.length > 0 &&
|
||||
(method[0].type === 'wildcard' ||
|
||||
(method[0].type === 'name' && method[0].name === value.property))
|
||||
) {
|
||||
if (method.length > 1) {
|
||||
newMethods.push(method.slice(1));
|
||||
} else {
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'LoadGlobal': {
|
||||
let macroDefinition = macroKinds.get(value.binding.name);
|
||||
if (macroDefinition != null) {
|
||||
macroTags.set(lvalue.identifier.id, macroDefinition);
|
||||
}
|
||||
break;
|
||||
if (newMethods.length > 0) {
|
||||
macroMethods.set(lvalue.identifier.id, newMethods);
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
if (typeof value.property === 'string') {
|
||||
const macroDefinition = macroTags.get(value.object.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
const propertyDefinition =
|
||||
macroDefinition.properties != null
|
||||
? (macroDefinition.properties.get(value.property) ??
|
||||
macroDefinition.properties.get('*'))
|
||||
: null;
|
||||
const propertyMacro = propertyDefinition ?? macroDefinition;
|
||||
macroTags.set(lvalue.identifier.id, propertyMacro);
|
||||
}
|
||||
} else if (isFbtCallExpression(fbtValues, value)) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* if the JSX element's tag was `fbt`, mark all its operands
|
||||
* to ensure that they end up in the same scope as the jsx element
|
||||
* itself.
|
||||
*/
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
fbtValues.add(operand.identifier.id);
|
||||
}
|
||||
} else if (
|
||||
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
|
||||
isFbtJsxChild(fbtValues, lvalue, value)
|
||||
) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* if the JSX element's tag was `fbt`, mark all its operands
|
||||
* to ensure that they end up in the same scope as the jsx element
|
||||
* itself.
|
||||
*/
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
|
||||
/*
|
||||
* NOTE: we add the operands as fbt values so that they are also
|
||||
* grouped with this expression
|
||||
*/
|
||||
fbtValues.add(operand.identifier.id);
|
||||
}
|
||||
} else if (fbtValues.has(lvalue.identifier.id)) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operand of eachReactiveValueOperand(value)) {
|
||||
if (
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
) {
|
||||
/*
|
||||
* named identifiers were already locals, we only have to force temporaries
|
||||
* into the same scope
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return macroTags;
|
||||
}
|
||||
|
||||
function mergeMacroArguments(
|
||||
fn: HIRFunction,
|
||||
macroTags: Map<IdentifierId, MacroDefinition>,
|
||||
macroKinds: Map<Macro, MacroDefinition>,
|
||||
): Set<IdentifierId> {
|
||||
const macroValues = new Set<IdentifierId>(macroTags.keys());
|
||||
for (const block of Array.from(fn.body.blocks.values()).reverse()) {
|
||||
for (let i = block.instructions.length - 1; i >= 0; i--) {
|
||||
const instr = block.instructions[i]!;
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'DeclareContext':
|
||||
case 'DeclareLocal':
|
||||
case 'Destructure':
|
||||
case 'LoadContext':
|
||||
case 'LoadLocal':
|
||||
case 'PostfixUpdate':
|
||||
case 'PrefixUpdate':
|
||||
case 'StoreContext':
|
||||
case 'StoreLocal': {
|
||||
// Instructions that never need to be merged
|
||||
break;
|
||||
}
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const macroDefinition =
|
||||
macroTags.get(callee.identifier.id) ??
|
||||
macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxExpression': {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
let macroDefinition;
|
||||
if (value.tag.kind === 'Identifier') {
|
||||
macroDefinition = macroTags.get(value.tag.identifier.id);
|
||||
} else {
|
||||
macroDefinition = macroKinds.get(value.tag.name);
|
||||
}
|
||||
macroDefinition ??= macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const macroDefinition = macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
|
||||
return Array.from(tags).some(macro =>
|
||||
typeof macro === 'string'
|
||||
? s === macro
|
||||
: macro[1].length === 0 && macro[0] === s,
|
||||
);
|
||||
}
|
||||
|
||||
function matchTagRoot(
|
||||
s: string,
|
||||
tags: Set<Macro>,
|
||||
): Array<Array<MacroMethod>> | null {
|
||||
const methods: Array<Array<MacroMethod>> = [];
|
||||
for (const macro of tags) {
|
||||
if (typeof macro === 'string') {
|
||||
continue;
|
||||
}
|
||||
for (const phi of block.phis) {
|
||||
const scope = phi.place.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const macroDefinition = macroTags.get(phi.place.identifier.id);
|
||||
if (
|
||||
macroDefinition == null ||
|
||||
macroDefinition.level === InlineLevel.Shallow
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
macroValues.add(phi.place.identifier.id);
|
||||
for (const operand of phi.operands.values()) {
|
||||
operand.identifier.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
|
||||
macroTags.set(operand.identifier.id, macroDefinition);
|
||||
macroValues.add(operand.identifier.id);
|
||||
}
|
||||
const [tag, rest] = macro;
|
||||
if (tag === s && rest.length > 0) {
|
||||
methods.push(rest);
|
||||
}
|
||||
}
|
||||
return macroValues;
|
||||
if (methods.length > 0) {
|
||||
return methods;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isFbtCallExpression(
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'CallExpression' &&
|
||||
fbtValues.has(value.callee.identifier.id)) ||
|
||||
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxExpression(
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
value.kind === 'JsxExpression' &&
|
||||
((value.tag.kind === 'Identifier' &&
|
||||
fbtValues.has(value.tag.identifier.id)) ||
|
||||
(value.tag.kind === 'BuiltinTag' &&
|
||||
matchesExactTag(value.tag.name, fbtMacroTags)))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxChild(
|
||||
fbtValues: Set<IdentifierId>,
|
||||
lvalue: Place | null,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
|
||||
lvalue !== null &&
|
||||
fbtValues.has(lvalue.identifier.id)
|
||||
);
|
||||
}
|
||||
|
||||
function expandFbtScopeRange(
|
||||
@@ -283,22 +275,3 @@ function expandFbtScopeRange(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function visitOperands(
|
||||
macroDefinition: MacroDefinition,
|
||||
scope: ReactiveScope,
|
||||
lvalue: Place,
|
||||
value: InstructionValue,
|
||||
macroValues: Set<IdentifierId>,
|
||||
macroTags: Map<IdentifierId, MacroDefinition>,
|
||||
): void {
|
||||
macroValues.add(lvalue.identifier.id);
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (macroDefinition.level === InlineLevel.Transitive) {
|
||||
operand.identifier.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
|
||||
macroTags.set(operand.identifier.id, macroDefinition);
|
||||
}
|
||||
macroValues.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ function* generateInstructionTypes(
|
||||
shapeId: BuiltInArrayId,
|
||||
});
|
||||
} else {
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
@@ -135,7 +135,16 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
} else if (val) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'customMacros' && typeof parsedVal === 'string') {
|
||||
maybeConfig[key] = [parsedVal.split('.')[0]];
|
||||
const valSplit = parsedVal.split('.');
|
||||
const props = [];
|
||||
for (const elt of valSplit.slice(1)) {
|
||||
if (elt === '*') {
|
||||
props.push({type: 'wildcard'});
|
||||
} else if (elt.length > 0) {
|
||||
props.push({type: 'name', name: elt});
|
||||
}
|
||||
}
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
continue;
|
||||
}
|
||||
maybeConfig[key] = parsedVal;
|
||||
|
||||
@@ -10,37 +10,16 @@ import {
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
const voidMemoErrors = new CompilerError();
|
||||
const useMemos = new Set<IdentifierId>();
|
||||
const react = new Set<IdentifierId>();
|
||||
const functions = new Map<IdentifierId, FunctionExpression>();
|
||||
const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const {lvalue, value} of block.instructions) {
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
/**
|
||||
* Most of the time useMemo results are referenced immediately. Don't bother
|
||||
* scanning instruction operands for useMemos unless there is an as-yet-unused
|
||||
* useMemo.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
unusedUseMemos.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
switch (value.kind) {
|
||||
case 'LoadGlobal': {
|
||||
if (value.binding.name === 'useMemo') {
|
||||
@@ -66,8 +45,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
case 'CallExpression': {
|
||||
// Is the function being called useMemo, with at least 1 argument?
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const isUseMemo = useMemos.has(callee.identifier.id);
|
||||
value.kind === 'CallExpression'
|
||||
? value.callee.identifier.id
|
||||
: value.property.identifier.id;
|
||||
const isUseMemo = useMemos.has(callee);
|
||||
if (!isUseMemo || value.args.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -123,106 +104,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
);
|
||||
}
|
||||
|
||||
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
|
||||
|
||||
if (fn.env.config.validateNoVoidUseMemo) {
|
||||
if (!hasNonVoidReturn(body.loweredFunc.func)) {
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: body.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
unusedUseMemos.delete(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unusedUseMemos.size !== 0) {
|
||||
/**
|
||||
* Basic check for unused memos, where the result of the call is never referenced. This runs
|
||||
* before DCE so it's more of an AST-level check that something, _anything_, cares about the value.
|
||||
*
|
||||
* This is easy to defeat with e.g. `const _ = useMemo(...)` but it at least gives us something to teach.
|
||||
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
|
||||
*/
|
||||
for (const loc of unusedUseMemos.values()) {
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() result is unused',
|
||||
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'useMemo() result is unused',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
fn.env.logErrors(voidMemoErrors.asResult());
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function validateNoContextVariableAssignment(
|
||||
fn: HIRFunction,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
const context = new Set(fn.context.map(place => place.identifier.id));
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const value = instr.value;
|
||||
switch (value.kind) {
|
||||
case 'StoreContext': {
|
||||
if (context.has(value.lvalue.place.identifier.id)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason:
|
||||
'useMemo() callbacks may not reassign variables declared outside of the callback',
|
||||
description:
|
||||
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: value.lvalue.place.loc,
|
||||
message: 'Cannot reassign variable',
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
|
||||
validateHooksUsage: 1,
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Invalid input: expected boolean, received number at "validateHooksUsage"."`,
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage"."`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('parseConfigPragma()', () => {
|
||||
],
|
||||
} as any);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: AutodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
|
||||
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
let x;
|
||||
const y = useMemo(() => {
|
||||
let z;
|
||||
x = [];
|
||||
z = true;
|
||||
return z;
|
||||
}, []);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: useMemo() callbacks may not reassign variables declared outside of the callback
|
||||
|
||||
useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
|
||||
|
||||
error.invalid-reassign-variable-in-usememo.ts:5:4
|
||||
3 | const y = useMemo(() => {
|
||||
4 | let z;
|
||||
> 5 | x = [];
|
||||
| ^ Cannot reassign variable
|
||||
6 | z = true;
|
||||
7 | return z;
|
||||
8 | }, []);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
function Component() {
|
||||
let x;
|
||||
const y = useMemo(() => {
|
||||
let z;
|
||||
x = [];
|
||||
z = true;
|
||||
return z;
|
||||
}, []);
|
||||
return [x, y];
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
// Intentionally don't bind state, this repros a bug where we didn't
|
||||
// infer the type of destructured properties after a hole in the array
|
||||
let [, setState] = useState();
|
||||
setState(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Calling setState during render may trigger an infinite loop
|
||||
|
||||
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
|
||||
|
||||
error.invalid-setState-in-render-unbound-state.ts:5:2
|
||||
3 | // infer the type of destructured properties after a hole in the array
|
||||
4 | let [, setState] = useState();
|
||||
> 5 | setState(1);
|
||||
| ^^^^^^^^ Found setState() in render
|
||||
6 | return props.foo;
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
function Component(props) {
|
||||
// Intentionally don't bind state, this repros a bug where we didn't
|
||||
// infer the type of destructured properties after a hole in the array
|
||||
let [, setState] = useState();
|
||||
setState(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
const value2 = React.useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: useMemo() callbacks must return a value
|
||||
|
||||
This useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.useMemo-no-return-value.ts:3:16
|
||||
1 | // @validateNoVoidUseMemo
|
||||
2 | function Component() {
|
||||
> 3 | const value = useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 4 | console.log('computing');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 5 | }, []);
|
||||
| ^^^^^^^^^ useMemo() callbacks must return a value
|
||||
6 | const value2 = React.useMemo(() => {
|
||||
7 | console.log('computing');
|
||||
8 | }, []);
|
||||
|
||||
Error: useMemo() callbacks must return a value
|
||||
|
||||
This React.useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.useMemo-no-return-value.ts:6:17
|
||||
4 | console.log('computing');
|
||||
5 | }, []);
|
||||
> 6 | const value2 = React.useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | console.log('computing');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 8 | }, []);
|
||||
| ^^^^^^^^^ useMemo() callbacks must return a value
|
||||
9 | return (
|
||||
10 | <div>
|
||||
11 | {value}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
// @validateNoVoidUseMemo
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
@@ -1,5 +1,9 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {identity} from 'shared-runtime';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
@@ -12,25 +16,22 @@ import {identity} from 'shared-runtime';
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
<Stringify>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', identity(firstname)),
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' + fbt.param('lastname', identity(lastname)),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,3 +40,17 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
---
|
||||
t3
|
||||
---
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fbt from 'fbt';
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
@@ -12,7 +12,7 @@ import {Stringify, identity} from 'shared-runtime';
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
<Stringify>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
@@ -20,18 +20,14 @@ function Component({firstname, lastname}) {
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' +
|
||||
fbt.param('lastname', <Stringify key={1} name={lastname} />),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,23 +44,15 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(5);
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] !== props.count || $[1] !== props.option) {
|
||||
let t1;
|
||||
if ($[3] !== props.count) {
|
||||
t1 = identity(props.count);
|
||||
$[3] = props.count;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = (
|
||||
<span>
|
||||
{fbt._(
|
||||
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
|
||||
[
|
||||
fbt._plural(t1, "count"),
|
||||
fbt._plural(identity(props.count), "count"),
|
||||
fbt._param(
|
||||
"option",
|
||||
|
||||
|
||||
@@ -44,23 +44,15 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(5);
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] !== props.count || $[1] !== props.option) {
|
||||
let t1;
|
||||
if ($[3] !== props.count) {
|
||||
t1 = identity(props.count);
|
||||
$[3] = props.count;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = (
|
||||
<span>
|
||||
{fbt._(
|
||||
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
|
||||
[
|
||||
fbt._plural(t1, "count"),
|
||||
fbt._plural(identity(props.count), "count"),
|
||||
fbt._param(
|
||||
"option",
|
||||
|
||||
|
||||
@@ -37,47 +37,28 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Foo(t0) {
|
||||
const $ = _c(13);
|
||||
const $ = _c(3);
|
||||
const { name1, name2 } = t0;
|
||||
let t1;
|
||||
if ($[0] !== name1 || $[1] !== name2) {
|
||||
let t2;
|
||||
if ($[3] !== name1) {
|
||||
t2 = <b>{name1}</b>;
|
||||
$[3] = name1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== name1 || $[6] !== t2) {
|
||||
t3 = <span key={name1}>{t2}</span>;
|
||||
$[5] = name1;
|
||||
$[6] = t2;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== name2) {
|
||||
t4 = <b>{name2}</b>;
|
||||
$[8] = name2;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== name2 || $[11] !== t4) {
|
||||
t5 = <span key={name2}>{t4}</span>;
|
||||
$[10] = name2;
|
||||
$[11] = t4;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
t1 = fbt._(
|
||||
"{user1} and {user2} accepted your PR!",
|
||||
[fbt._param("user1", t3), fbt._param("user2", t5)],
|
||||
[
|
||||
fbt._param(
|
||||
"user1",
|
||||
|
||||
<span key={name1}>
|
||||
<b>{name1}</b>
|
||||
</span>,
|
||||
),
|
||||
fbt._param(
|
||||
"user2",
|
||||
|
||||
<span key={name2}>
|
||||
<b>{name2}</b>
|
||||
</span>,
|
||||
),
|
||||
],
|
||||
{ hk: "2PxMie" },
|
||||
);
|
||||
$[0] = name1;
|
||||
|
||||
@@ -29,24 +29,20 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const $ = _c(4);
|
||||
const { name, data, icon } = t0;
|
||||
let t1;
|
||||
if ($[0] !== data || $[1] !== icon || $[2] !== name) {
|
||||
let t2;
|
||||
if ($[4] !== name) {
|
||||
t2 = <Text type="h4">{name}</Text>;
|
||||
$[4] = name;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
t1 = (
|
||||
<Text type="body4">
|
||||
{fbt._(
|
||||
"{item author}{icon}{=m2}",
|
||||
[
|
||||
fbt._param("item author", t2),
|
||||
fbt._param(
|
||||
"item author",
|
||||
|
||||
<Text type="h4">{name}</Text>,
|
||||
),
|
||||
fbt._param(
|
||||
"icon",
|
||||
|
||||
|
||||
@@ -27,21 +27,16 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.text) {
|
||||
const t1 = identity(props.text);
|
||||
let t2;
|
||||
if ($[2] !== t1) {
|
||||
t2 = <>{t1}</>;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
t0 = (
|
||||
<Foo
|
||||
value={fbt._("{value}%", [fbt._param("value", t2)], { hk: "10F5Cc" })}
|
||||
value={fbt._(
|
||||
"{value}%",
|
||||
[fbt._param("value", <>{identity(props.text)}</>)],
|
||||
{ hk: "10F5Cc" },
|
||||
)}
|
||||
/>
|
||||
);
|
||||
$[0] = props.text;
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Example({x}) {
|
||||
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
|
||||
// be memoized separately
|
||||
return (
|
||||
<fbt desc="Description">
|
||||
Outer Text
|
||||
<Foo key="b" x={x}>
|
||||
<Bar key="a">Inner Text</Bar>
|
||||
</Foo>
|
||||
</fbt>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo({x, children}) {
|
||||
'use no memo';
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
'use no memo';
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{x: 'Hello'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { fbt } from "fbt";
|
||||
|
||||
function Example(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = fbt._(
|
||||
"Outer Text {=m1}",
|
||||
[
|
||||
fbt._implicitParam(
|
||||
"=m1",
|
||||
|
||||
<Foo key="b" x={x}>
|
||||
{fbt._(
|
||||
"{=m1}",
|
||||
[
|
||||
fbt._implicitParam(
|
||||
"=m1",
|
||||
<Bar key="a">
|
||||
{fbt._("Inner Text", null, { hk: "32YB0l" })}
|
||||
</Bar>,
|
||||
),
|
||||
],
|
||||
{ hk: "23dJsI" },
|
||||
)}
|
||||
</Foo>,
|
||||
),
|
||||
],
|
||||
{ hk: "2RVA7V" },
|
||||
);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
function Foo({ x, children }) {
|
||||
"use no memo";
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({ children }) {
|
||||
"use no memo";
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{ x: "Hello" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) Outer Text <div>Hello</div><span>Inner Text</span>
|
||||
@@ -1,35 +0,0 @@
|
||||
// @flow
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Example({x}) {
|
||||
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
|
||||
// be memoized separately
|
||||
return (
|
||||
<fbt desc="Description">
|
||||
Outer Text
|
||||
<Foo key="b" x={x}>
|
||||
<Bar key="a">Inner Text</Bar>
|
||||
</Foo>
|
||||
</fbt>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo({x, children}) {
|
||||
'use no memo';
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
'use no memo';
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{x: 'Hello'}],
|
||||
};
|
||||
@@ -1,128 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' +
|
||||
fbt.param('lastname', <Stringify key={1} name={lastname} />),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
import { Stringify, identity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(9);
|
||||
const { firstname, lastname } = t0;
|
||||
let t1;
|
||||
if ($[0] !== firstname || $[1] !== lastname) {
|
||||
let t2;
|
||||
if ($[3] !== firstname) {
|
||||
t2 = <Stringify key={0} name={firstname} />;
|
||||
$[3] = firstname;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== lastname) {
|
||||
t3 = <Stringify key={1} name={lastname} />;
|
||||
$[5] = lastname;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
t1 = fbt._(
|
||||
"Name: {firstname}, {lastname}",
|
||||
[
|
||||
fbt._param("firstname", t2),
|
||||
fbt._param(
|
||||
"lastname",
|
||||
identity(
|
||||
fbt._("(inner){lastname}", [fbt._param("lastname", t3)], {
|
||||
hk: "1Kdxyo",
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
{ hk: "3AiIf8" },
|
||||
);
|
||||
$[0] = firstname;
|
||||
$[1] = lastname;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== t1) {
|
||||
t2 = <div>{t1}</div>;
|
||||
$[7] = t1;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstname: "first", lastname: "last" }],
|
||||
sequentialRenders: [{ firstname: "first", lastname: "last" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Name: <div>{"name":"first"}</div>, (inner)<div>{"name":"last"}</div></div>
|
||||
@@ -1,124 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', identity(firstname)),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' + fbt.param('lastname', identity(lastname)),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(5);
|
||||
const { firstname, lastname } = t0;
|
||||
let t1;
|
||||
if ($[0] !== firstname || $[1] !== lastname) {
|
||||
t1 = fbt._(
|
||||
"Name: {firstname}, {lastname}",
|
||||
[
|
||||
fbt._param(
|
||||
"firstname",
|
||||
|
||||
identity(firstname),
|
||||
),
|
||||
fbt._param(
|
||||
"lastname",
|
||||
|
||||
identity(
|
||||
fbt._(
|
||||
"(inner){lastname}",
|
||||
[
|
||||
fbt._param(
|
||||
"lastname",
|
||||
|
||||
identity(lastname),
|
||||
),
|
||||
],
|
||||
{ hk: "1Kdxyo" },
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
{ hk: "3AiIf8" },
|
||||
);
|
||||
$[0] = firstname;
|
||||
$[1] = lastname;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = <div>{t1}</div>;
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstname: "first", lastname: "last" }],
|
||||
sequentialRenders: [{ firstname: "first", lastname: "last" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Name: first, (inner)last</div>
|
||||
@@ -1,78 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {fbt} from 'fbt';
|
||||
import {useState} from 'react';
|
||||
|
||||
const MIN = 10;
|
||||
|
||||
function Component() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return fbt(
|
||||
'Expected at least ' +
|
||||
fbt.param('min', MIN, {number: true}) +
|
||||
' items, but got ' +
|
||||
fbt.param('count', count, {number: true}) +
|
||||
' items.',
|
||||
'Error description'
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { fbt } from "fbt";
|
||||
import { useState } from "react";
|
||||
|
||||
const MIN = 10;
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const [count] = useState(0);
|
||||
let t0;
|
||||
if ($[0] !== count) {
|
||||
t0 = fbt._(
|
||||
{ "*": { "*": "Expected at least {min} items, but got {count} items." } },
|
||||
[
|
||||
fbt._param(
|
||||
"min",
|
||||
|
||||
MIN,
|
||||
[0],
|
||||
),
|
||||
fbt._param(
|
||||
"count",
|
||||
|
||||
count,
|
||||
[0],
|
||||
),
|
||||
],
|
||||
{ hk: "36gbz8" },
|
||||
);
|
||||
$[0] = count;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) Expected at least 10 items, but got 0 items.
|
||||
@@ -1,22 +0,0 @@
|
||||
import {fbt} from 'fbt';
|
||||
import {useState} from 'react';
|
||||
|
||||
const MIN = 10;
|
||||
|
||||
function Component() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return fbt(
|
||||
'Expected at least ' +
|
||||
fbt.param('min', MIN, {number: true}) +
|
||||
' items, but got ' +
|
||||
fbt.param('count', count, {number: true}) +
|
||||
' items.',
|
||||
'Error description'
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function t(props) {
|
||||
let [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function t(props) {
|
||||
const [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ["TodoAdd"],
|
||||
isComponent: "TodoAdd",
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
function t(props) {
|
||||
let [, setstate] = useState();
|
||||
setstate(1);
|
||||
return props.foo;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: t,
|
||||
params: ['TodoAdd'],
|
||||
isComponent: 'TodoAdd',
|
||||
};
|
||||
@@ -37,7 +37,7 @@ function Component(props) {
|
||||
const $ = _c(16);
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = idx(props, (_) => _.group.label);
|
||||
t0 = idx(props, _temp);
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
@@ -46,7 +46,7 @@ function Component(props) {
|
||||
const groupName1 = t0;
|
||||
let t1;
|
||||
if ($[2] !== props) {
|
||||
t1 = idx.a(props, (__0) => __0.group.label);
|
||||
t1 = idx.a(props, _temp2);
|
||||
$[2] = props;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
@@ -73,7 +73,7 @@ function Component(props) {
|
||||
const groupName4 = t3;
|
||||
let t4;
|
||||
if ($[8] !== props) {
|
||||
t4 = idx.hello_world.b.c(props, (__3) => __3.group.label);
|
||||
t4 = idx.hello_world.b.c(props, _temp3);
|
||||
$[8] = props;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
@@ -108,6 +108,15 @@ function Component(props) {
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
function _temp3(__3) {
|
||||
return __3.group.label;
|
||||
}
|
||||
function _temp2(__0) {
|
||||
return __0.group.label;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.group.label;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ function Component(props) {
|
||||
const $ = _c(10);
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = idx(props, (_) => _.group.label);
|
||||
t0 = idx(props, _temp);
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
@@ -49,7 +49,7 @@ function Component(props) {
|
||||
const groupName2 = t1;
|
||||
let t2;
|
||||
if ($[4] !== props) {
|
||||
t2 = idx.a.b(props, (__1) => __1.group.label);
|
||||
t2 = idx.a.b(props, _temp2);
|
||||
$[4] = props;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
@@ -74,6 +74,12 @@ function Component(props) {
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
function _temp2(__1) {
|
||||
return __1.group.label;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.group.label;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() result is unused","description":"This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":2,"index":67},"end":{"line":3,"column":9,"index":74},"filename":"invalid-unused-usememo.ts","identifierName":"useMemo"},"message":"useMemo() result is unused"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":7,"column":1,"index":127},"filename":"invalid-unused-usememo.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,7 +0,0 @@
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
const value2 = React.useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
|
||||
console.log("computing");
|
||||
|
||||
console.log("computing");
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (
|
||||
<div>
|
||||
{undefined}
|
||||
{undefined}
|
||||
</div>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":24,"index":89},"end":{"line":5,"column":3,"index":130},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":6,"column":31,"index":168},"end":{"line":8,"column":3,"index":209},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":15,"column":1,"index":283},"filename":"invalid-useMemo-no-return-value.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
}, []);
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
mutate(a);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":18,"index":61},"end":{"line":5,"column":3,"index":87},"filename":"invalid-useMemo-return-empty.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":19},"end":{"line":7,"column":1,"index":107},"filename":"invalid-useMemo-return-empty.ts"},"fnName":"component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":1,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const {x, ...rest} = useIdentity(props);
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const t0 = useIdentity(props);
|
||||
let rest;
|
||||
let x;
|
||||
if ($[0] !== t0) {
|
||||
({ x, ...rest } = t0);
|
||||
$[0] = t0;
|
||||
$[1] = rest;
|
||||
$[2] = x;
|
||||
} else {
|
||||
rest = $[1];
|
||||
x = $[2];
|
||||
}
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
let t1;
|
||||
if ($[3] !== x || $[4] !== z) {
|
||||
t1 = <Stringify x={x} z={z} />;
|
||||
$[3] = x;
|
||||
$[4] = z;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: "Hello", z: "World" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":"Hello","z":"World"}</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const {x, ...rest} = useIdentity(props);
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
return <Stringify {...rest} x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
let rest;
|
||||
let x;
|
||||
if ($[0] !== t0) {
|
||||
({ x, ...rest } = t0);
|
||||
$[0] = t0;
|
||||
$[1] = rest;
|
||||
$[2] = x;
|
||||
} else {
|
||||
rest = $[1];
|
||||
x = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== rest || $[4] !== x) {
|
||||
t1 = <Stringify {...rest} x={x} />;
|
||||
$[3] = rest;
|
||||
$[4] = x;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: "Hello", z: "World" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"z":"World","x":"Hello"}</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
return <Stringify {...rest} x={x} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
const restAlias = rest;
|
||||
const z = restAlias.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
let rest;
|
||||
let x;
|
||||
if ($[0] !== t0) {
|
||||
({ x, ...rest } = t0);
|
||||
$[0] = t0;
|
||||
$[1] = rest;
|
||||
$[2] = x;
|
||||
} else {
|
||||
rest = $[1];
|
||||
x = $[2];
|
||||
}
|
||||
const restAlias = rest;
|
||||
const z = restAlias.z;
|
||||
identity(z);
|
||||
let t1;
|
||||
if ($[3] !== x || $[4] !== z) {
|
||||
t1 = <Stringify x={x} z={z} />;
|
||||
$[3] = x;
|
||||
$[4] = z;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: "Hello", z: "World" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":"Hello","z":"World"}</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
const restAlias = rest;
|
||||
const z = restAlias.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
let rest;
|
||||
let x;
|
||||
if ($[0] !== t0) {
|
||||
({ x, ...rest } = t0);
|
||||
$[0] = t0;
|
||||
$[1] = rest;
|
||||
$[2] = x;
|
||||
} else {
|
||||
rest = $[1];
|
||||
x = $[2];
|
||||
}
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
let t1;
|
||||
if ($[3] !== x || $[4] !== z) {
|
||||
t1 = <Stringify x={x} z={z} />;
|
||||
$[3] = x;
|
||||
$[4] = z;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: "Hello", z: "World" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"x":"Hello","z":"World"}</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, ...rest}) {
|
||||
const z = rest.z;
|
||||
identity(z);
|
||||
return <Stringify x={x} z={z} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 'Hello', z: 'World'}],
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* isPending intentionally not captured */ start] = useTransition();
|
||||
|
||||
return useCallback(() => {
|
||||
start();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useCallback, useTransition } from "react";
|
||||
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
const [, start] = useTransition();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
start();
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) "[[ function params=0 ]]"
|
||||
@@ -1,15 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* isPending intentionally not captured */ start] = useTransition();
|
||||
|
||||
return useCallback(() => {
|
||||
start();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* state value intentionally not captured */ setState] = useState();
|
||||
|
||||
return useCallback(() => {
|
||||
setState(x => x + 1);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useCallback, useTransition } from "react";
|
||||
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
const [, setState] = useState();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
setState(_temp);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp(x) {
|
||||
return x + 1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) useState is not defined
|
||||
@@ -1,15 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback, useTransition} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [, /* state value intentionally not captured */ setState] = useState();
|
||||
|
||||
return useCallback(() => {
|
||||
setState(x => x + 1);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
export hook useItemLanguage(items) {
|
||||
return useMemo(() => {
|
||||
let language: ?string = null;
|
||||
items.forEach(item => {
|
||||
if (item.language != null) {
|
||||
language = item.language;
|
||||
}
|
||||
});
|
||||
return language;
|
||||
}, [items]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export function useItemLanguage(items) {
|
||||
const $ = _c(2);
|
||||
let language;
|
||||
if ($[0] !== items) {
|
||||
language = null;
|
||||
items.forEach((item) => {
|
||||
if (item.language != null) {
|
||||
language = item.language;
|
||||
}
|
||||
});
|
||||
$[0] = items;
|
||||
$[1] = language;
|
||||
} else {
|
||||
language = $[1];
|
||||
}
|
||||
return language;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
export hook useItemLanguage(items) {
|
||||
return useMemo(() => {
|
||||
let language: ?string = null;
|
||||
items.forEach(item => {
|
||||
if (item.language != null) {
|
||||
language = item.language;
|
||||
}
|
||||
});
|
||||
return language;
|
||||
}, [items]);
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const item = props.item;
|
||||
const thumbnails = [];
|
||||
@@ -23,7 +22,7 @@ function Component(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo:false
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const item = props.item;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const item = props.item;
|
||||
const thumbnails = [];
|
||||
|
||||
@@ -6,7 +6,6 @@ function Component(props) {
|
||||
const x = useMemo(() => {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
return props.value;
|
||||
}
|
||||
}
|
||||
}, [props.cond]);
|
||||
@@ -25,18 +24,10 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
let t0;
|
||||
bb0: {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
t0 = props.value;
|
||||
break bb0;
|
||||
}
|
||||
}
|
||||
t0 = undefined;
|
||||
}
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -2,7 +2,6 @@ function Component(props) {
|
||||
const x = useMemo(() => {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
return props.value;
|
||||
}
|
||||
}
|
||||
}, [props.cond]);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
}, []);
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function component(a) {
|
||||
mutate(a);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
@@ -14,7 +14,6 @@ export {
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
LintRules,
|
||||
LintRulePreset,
|
||||
type CompilerErrorDetailOptions,
|
||||
type CompilerDiagnosticOptions,
|
||||
type CompilerDiagnosticDetail,
|
||||
|
||||
@@ -5,15 +5,22 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('no impure function calls rule', ReactCompilerRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Known impure function calls are caught',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'no impure function calls rule',
|
||||
allRules[getRuleForCategory(ErrorCategory.Purity).name].rule,
|
||||
{
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Known impure function calls are caught',
|
||||
code: normalizeIndent`
|
||||
function Component() {
|
||||
const date = Date.now();
|
||||
const now = performance.now();
|
||||
@@ -21,11 +28,12 @@ testRule('no impure function calls rule', ReactCompilerRule, {
|
||||
return <Foo date={date} now={now} rand={rand} />;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,23 +5,30 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, makeTestCaseError, testRule} from './shared-utils';
|
||||
import {AllRules} from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('rules-of-hooks', AllRules, {
|
||||
valid: [
|
||||
{
|
||||
name: 'Basic example',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'rules-of-hooks',
|
||||
allRules[getRuleForCategory(ErrorCategory.Hooks).name].rule,
|
||||
{
|
||||
valid: [
|
||||
{
|
||||
name: 'Basic example',
|
||||
code: normalizeIndent`
|
||||
function Component() {
|
||||
useHook();
|
||||
return <div>Hello world</div>;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Violation with Flow suppression',
|
||||
code: `
|
||||
},
|
||||
{
|
||||
name: 'Violation with Flow suppression',
|
||||
code: `
|
||||
// Valid since error already suppressed with flow.
|
||||
function useHook() {
|
||||
if (cond) {
|
||||
@@ -30,11 +37,11 @@ testRule('rules-of-hooks', AllRules, {
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
// OK because invariants are only meant for the compiler team's consumption
|
||||
name: '[Invariant] Defined after use',
|
||||
code: normalizeIndent`
|
||||
},
|
||||
{
|
||||
// OK because invariants are only meant for the compiler team's consumption
|
||||
name: '[Invariant] Defined after use',
|
||||
code: normalizeIndent`
|
||||
function Component(props) {
|
||||
let y = function () {
|
||||
m(x);
|
||||
@@ -45,42 +52,49 @@ testRule('rules-of-hooks', AllRules, {
|
||||
return y;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Classes don't throw",
|
||||
code: normalizeIndent`
|
||||
},
|
||||
{
|
||||
name: "Classes don't throw",
|
||||
code: normalizeIndent`
|
||||
class Foo {
|
||||
#bar() {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Simple violation',
|
||||
code: normalizeIndent`
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Simple violation',
|
||||
code: normalizeIndent`
|
||||
function useConditional() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Hooks must always be called in a consistent order'),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Multiple diagnostics within the same function are surfaced',
|
||||
code: normalizeIndent`
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Hooks must always be called in a consistent order',
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Multiple diagnostics within the same function are surfaced',
|
||||
code: normalizeIndent`
|
||||
function useConditional() {
|
||||
cond ?? useConditionalHook();
|
||||
props.cond && useConditionalHook();
|
||||
return <div>Hello world</div>;
|
||||
}`,
|
||||
errors: [
|
||||
makeTestCaseError('Hooks must always be called in a consistent order'),
|
||||
makeTestCaseError('Hooks must always be called in a consistent order'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Hooks must always be called in a consistent order',
|
||||
),
|
||||
makeTestCaseError(
|
||||
'Hooks must always be called in a consistent order',
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,15 +5,22 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('no ambiguous JSX rule', ReactCompilerRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'JSX in try blocks are warned against',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'no ambiguous JSX rule',
|
||||
allRules[getRuleForCategory(ErrorCategory.ErrorBoundaries).name].rule,
|
||||
{
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'JSX in try blocks are warned against',
|
||||
code: normalizeIndent`
|
||||
function Component(props) {
|
||||
let el;
|
||||
try {
|
||||
@@ -24,7 +31,8 @@ testRule('no ambiguous JSX rule', ReactCompilerRule, {
|
||||
return el;
|
||||
}
|
||||
`,
|
||||
errors: [makeTestCaseError('Avoid constructing JSX within try/catch')],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [makeTestCaseError('Avoid constructing JSX within try/catch')],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,15 +4,22 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, makeTestCaseError, testRule} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('no-capitalized-calls', ReactCompilerRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Simple violation',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'no-capitalized-calls',
|
||||
allRules[getRuleForCategory(ErrorCategory.CapitalizedCalls).name].rule,
|
||||
{
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Simple violation',
|
||||
code: normalizeIndent`
|
||||
import Child from './Child';
|
||||
function Component() {
|
||||
return <>
|
||||
@@ -20,13 +27,15 @@ testRule('no-capitalized-calls', ReactCompilerRule, {
|
||||
</>;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Capitalized functions are reserved for components'),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Method call violation',
|
||||
code: normalizeIndent`
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Capitalized functions are reserved for components',
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Method call violation',
|
||||
code: normalizeIndent`
|
||||
import myModule from './MyModule';
|
||||
function Component() {
|
||||
return <>
|
||||
@@ -34,13 +43,15 @@ testRule('no-capitalized-calls', ReactCompilerRule, {
|
||||
</>;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Capitalized functions are reserved for components'),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Multiple diagnostics within the same function are surfaced',
|
||||
code: normalizeIndent`
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Capitalized functions are reserved for components',
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Multiple diagnostics within the same function are surfaced',
|
||||
code: normalizeIndent`
|
||||
import Child1 from './Child1';
|
||||
import MyModule from './MyModule';
|
||||
function Component() {
|
||||
@@ -49,9 +60,12 @@ testRule('no-capitalized-calls', ReactCompilerRule, {
|
||||
{MyModule.Child2()}
|
||||
</>;
|
||||
}`,
|
||||
errors: [
|
||||
makeTestCaseError('Capitalized functions are reserved for components'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [
|
||||
makeTestCaseError(
|
||||
'Capitalized functions are reserved for components',
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,22 +5,30 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('no ref access in render rule', ReactCompilerRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'validate against simple ref access in render',
|
||||
code: normalizeIndent`
|
||||
testRule(
|
||||
'no ref access in render rule',
|
||||
allRules[getRuleForCategory(ErrorCategory.Refs).name].rule,
|
||||
{
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: 'validate against simple ref access in render',
|
||||
code: normalizeIndent`
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
const value = ref.current;
|
||||
return value;
|
||||
}
|
||||
`,
|
||||
errors: [makeTestCaseError('Cannot access refs during render')],
|
||||
},
|
||||
],
|
||||
});
|
||||
errors: [makeTestCaseError('Cannot access refs during render')],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 {NoUnusedDirectivesRule} from '../src/rules/ReactCompilerRule';
|
||||
import {normalizeIndent, testRule} from './shared-utils';
|
||||
|
||||
testRule('no unused directives rule', NoUnusedDirectivesRule, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
name: "Unused 'use no forget' directive is reported when no errors are present on components",
|
||||
code: normalizeIndent`
|
||||
function Component() {
|
||||
'use no forget';
|
||||
return <div>Hello world</div>
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: "Unused 'use no forget' directive",
|
||||
suggestions: [
|
||||
{
|
||||
output:
|
||||
// yuck
|
||||
'\nfunction Component() {\n \n return <div>Hello world</div>\n}\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Unused 'use no forget' directive is reported when no errors are present on non-components or hooks",
|
||||
code: normalizeIndent`
|
||||
function notacomponent() {
|
||||
'use no forget';
|
||||
return 1 + 1;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: "Unused 'use no forget' directive",
|
||||
suggestions: [
|
||||
{
|
||||
output:
|
||||
// yuck
|
||||
'\nfunction notacomponent() {\n \n return 1 + 1;\n}\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -5,10 +5,19 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils';
|
||||
import {AllRules} from '../src/rules/ReactCompilerRule';
|
||||
import {
|
||||
ErrorCategory,
|
||||
getRuleForCategory,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import {
|
||||
normalizeIndent,
|
||||
testRule,
|
||||
makeTestCaseError,
|
||||
TestRecommendedRules,
|
||||
} from './shared-utils';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
testRule('plugin-recommended', AllRules, {
|
||||
testRule('plugin-recommended', TestRecommendedRules, {
|
||||
valid: [
|
||||
{
|
||||
name: 'Basic example with component syntax',
|
||||
@@ -111,15 +120,7 @@ testRule('plugin-recommended', AllRules, {
|
||||
|
||||
return <Child x={state} />;
|
||||
}`,
|
||||
errors: [
|
||||
makeTestCaseError('useMemo() callbacks must return a value'),
|
||||
makeTestCaseError(
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
),
|
||||
makeTestCaseError(
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
),
|
||||
],
|
||||
errors: [makeTestCaseError('useMemo() callbacks must return a value')],
|
||||
},
|
||||
{
|
||||
name: 'Pipeline errors are reported',
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
*/
|
||||
|
||||
import {RuleTester} from 'eslint';
|
||||
import {CompilerTestCases, normalizeIndent} from './shared-utils';
|
||||
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
|
||||
import {
|
||||
CompilerTestCases,
|
||||
normalizeIndent,
|
||||
TestRecommendedRules,
|
||||
} from './shared-utils';
|
||||
|
||||
const tests: CompilerTestCases = {
|
||||
valid: [
|
||||
@@ -59,4 +62,4 @@ const eslintTester = new RuleTester({
|
||||
// @ts-ignore[2353] - outdated types
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
});
|
||||
eslintTester.run('react-compiler', ReactCompilerRule, tests);
|
||||
eslintTester.run('react-compiler', TestRecommendedRules, tests);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import {RuleTester as ESLintTester, Rule} from 'eslint';
|
||||
import {type ErrorCategory} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
import escape from 'regexp.escape';
|
||||
import {configs} from '../src/index';
|
||||
import {allRules} from '../src/rules/ReactCompilerRule';
|
||||
|
||||
/**
|
||||
* A string template tag that removes padding from the left side of multi-line strings
|
||||
@@ -43,4 +46,31 @@ export function testRule(
|
||||
eslintTester.run(name, rule, tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates all recommended rules from the plugin.
|
||||
*/
|
||||
export const TestRecommendedRules: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow capitalized function calls',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
},
|
||||
// validation is done at runtime with zod
|
||||
schema: [{type: 'object', additionalProperties: true}],
|
||||
},
|
||||
create(context) {
|
||||
for (const ruleConfig of Object.values(
|
||||
configs.recommended.plugins['react-compiler'].rules,
|
||||
)) {
|
||||
const listener = ruleConfig.rule.create(context);
|
||||
if (Object.entries(listener).length !== 0) {
|
||||
throw new Error('TODO: handle rules that return listeners to eslint');
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
test('no test', () => {});
|
||||
|
||||
@@ -10,5 +10,6 @@ module.exports = {
|
||||
plugins: [
|
||||
['@babel/plugin-transform-private-property-in-object', {loose: true}],
|
||||
['@babel/plugin-transform-class-properties', {loose: true}],
|
||||
['@babel/plugin-transform-private-methods', {loose: true}],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"hermes-parser": "^0.25.1",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.4",
|
||||
|
||||
@@ -5,24 +5,37 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import ReactCompilerRule from './rules/ReactCompilerRule';
|
||||
import {type Linter} from 'eslint';
|
||||
import {
|
||||
allRules,
|
||||
mapErrorSeverityToESlint,
|
||||
recommendedRules,
|
||||
} from './rules/ReactCompilerRule';
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
'react-compiler': ReactCompilerRule,
|
||||
},
|
||||
configs: {
|
||||
recommended: {
|
||||
plugins: {
|
||||
'react-compiler': {
|
||||
rules: {
|
||||
'react-compiler': ReactCompilerRule,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react-compiler/react-compiler': 'error',
|
||||
const meta = {
|
||||
name: 'eslint-plugin-react-compiler',
|
||||
};
|
||||
|
||||
const configs = {
|
||||
recommended: {
|
||||
plugins: {
|
||||
'react-compiler': {
|
||||
rules: allRules,
|
||||
},
|
||||
},
|
||||
rules: Object.fromEntries(
|
||||
Object.entries(recommendedRules).map(([name, ruleConfig]) => {
|
||||
return [
|
||||
'react-compiler/' + name,
|
||||
mapErrorSeverityToESlint(ruleConfig.severity),
|
||||
];
|
||||
}),
|
||||
) as Record<string, Linter.StringSeverity>,
|
||||
},
|
||||
};
|
||||
|
||||
const rules = Object.fromEntries(
|
||||
Object.entries(allRules).map(([name, {rule}]) => [name, rule]),
|
||||
);
|
||||
|
||||
export {configs, rules, meta};
|
||||
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
import type {Linter, Rule} from 'eslint';
|
||||
import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler';
|
||||
import {
|
||||
ErrorCategory,
|
||||
LintRulePreset,
|
||||
ErrorSeverity,
|
||||
LintRules,
|
||||
type LintRule,
|
||||
} from 'babel-plugin-react-compiler/src/CompilerError';
|
||||
@@ -108,15 +107,14 @@ function hasFlowSuppression(
|
||||
return false;
|
||||
}
|
||||
|
||||
function makeRule(rules: Array<LintRule>): Rule.RuleModule {
|
||||
const categories = new Set(rules.map(rule => rule.category));
|
||||
function makeRule(rule: LintRule): Rule.RuleModule {
|
||||
const create = (context: Rule.RuleContext): Rule.RuleListener => {
|
||||
const result = getReactCompilerResult(context);
|
||||
|
||||
for (const event of result.events) {
|
||||
if (event.kind === 'CompileError') {
|
||||
const detail = event.detail;
|
||||
if (categories.has(detail.category)) {
|
||||
if (detail.category === rule.category) {
|
||||
const loc = detail.primaryLocation();
|
||||
if (loc == null || typeof loc === 'symbol') {
|
||||
continue;
|
||||
@@ -151,8 +149,8 @@ function makeRule(rules: Array<LintRule>): Rule.RuleModule {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'React Compiler diagnostics',
|
||||
recommended: true,
|
||||
description: rule.description,
|
||||
recommended: rule.recommended,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
@@ -163,13 +161,86 @@ function makeRule(rules: Array<LintRule>): Rule.RuleModule {
|
||||
};
|
||||
}
|
||||
|
||||
export default makeRule(
|
||||
LintRules.filter(
|
||||
rule =>
|
||||
rule.preset === LintRulePreset.Recommended ||
|
||||
rule.preset === LintRulePreset.RecommendedLatest ||
|
||||
rule.category === ErrorCategory.CapitalizedCalls,
|
||||
),
|
||||
export const NoUnusedDirectivesRule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
// validation is done at runtime with zod
|
||||
schema: [{type: 'object', additionalProperties: true}],
|
||||
},
|
||||
create(context: Rule.RuleContext): Rule.RuleListener {
|
||||
const results = getReactCompilerResult(context);
|
||||
|
||||
for (const directive of results.unusedOptOutDirectives) {
|
||||
context.report({
|
||||
message: `Unused '${directive.directive}' directive`,
|
||||
loc: directive.loc,
|
||||
suggest: [
|
||||
{
|
||||
desc: 'Remove the directive',
|
||||
fix(fixer): Rule.Fix {
|
||||
return fixer.removeRange(directive.range);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
type RulesConfig = {
|
||||
[name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
|
||||
};
|
||||
|
||||
export const allRules: RulesConfig = LintRules.reduce(
|
||||
(acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
'no-unused-directives': {
|
||||
rule: NoUnusedDirectivesRule,
|
||||
severity: ErrorSeverity.Error,
|
||||
},
|
||||
} as RulesConfig,
|
||||
);
|
||||
|
||||
export const AllRules = makeRule(LintRules);
|
||||
export const recommendedRules: RulesConfig = LintRules.filter(
|
||||
rule => rule.recommended,
|
||||
).reduce(
|
||||
(acc, rule) => {
|
||||
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
'no-unused-directives': {
|
||||
rule: NoUnusedDirectivesRule,
|
||||
severity: ErrorSeverity.Error,
|
||||
},
|
||||
} as RulesConfig,
|
||||
);
|
||||
|
||||
export function mapErrorSeverityToESlint(
|
||||
severity: ErrorSeverity,
|
||||
): Linter.StringSeverity {
|
||||
switch (severity) {
|
||||
case ErrorSeverity.Error: {
|
||||
return 'error';
|
||||
}
|
||||
case ErrorSeverity.Warning: {
|
||||
return 'warn';
|
||||
}
|
||||
case ErrorSeverity.Hint:
|
||||
case ErrorSeverity.Off: {
|
||||
return 'off';
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(severity, `Unhandled severity: ${severity}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,20 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {transformFromAstSync, traverse} from '@babel/core';
|
||||
import {parse as babelParse} from '@babel/parser';
|
||||
import {File} from '@babel/types';
|
||||
import {Directive, File} from '@babel/types';
|
||||
// @ts-expect-error: no types available
|
||||
import PluginProposalPrivateMethods from '@babel/plugin-proposal-private-methods';
|
||||
import BabelPluginReactCompiler, {
|
||||
parsePluginOptions,
|
||||
validateEnvironmentConfig,
|
||||
OPT_OUT_DIRECTIVES,
|
||||
type PluginOptions,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import {Logger, LoggerEvent} from 'babel-plugin-react-compiler/src/Entrypoint';
|
||||
import type {SourceCode} from 'eslint';
|
||||
import {SourceLocation} from 'estree';
|
||||
// @ts-expect-error: no types available
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import {isDeepStrictEqual} from 'util';
|
||||
@@ -41,11 +45,17 @@ const COMPILER_OPTIONS: PluginOptions = {
|
||||
}),
|
||||
};
|
||||
|
||||
export type UnusedOptOutDirective = {
|
||||
loc: SourceLocation;
|
||||
range: [number, number];
|
||||
directive: string;
|
||||
};
|
||||
export type RunCacheEntry = {
|
||||
sourceCode: string;
|
||||
filename: string;
|
||||
userOpts: PluginOptions;
|
||||
flowSuppressions: Array<{line: number; code: string}>;
|
||||
unusedOptOutDirectives: Array<UnusedOptOutDirective>;
|
||||
events: Array<LoggerEvent>;
|
||||
};
|
||||
|
||||
@@ -77,6 +87,25 @@ function getFlowSuppressions(
|
||||
return results;
|
||||
}
|
||||
|
||||
function filterUnusedOptOutDirectives(
|
||||
directives: ReadonlyArray<Directive>,
|
||||
): Array<UnusedOptOutDirective> {
|
||||
const results: Array<UnusedOptOutDirective> = [];
|
||||
for (const directive of directives) {
|
||||
if (
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value) &&
|
||||
directive.loc != null
|
||||
) {
|
||||
results.push({
|
||||
loc: directive.loc,
|
||||
directive: directive.value.value,
|
||||
range: [directive.start!, directive.end!],
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function runReactCompilerImpl({
|
||||
sourceCode,
|
||||
filename,
|
||||
@@ -96,6 +125,7 @@ function runReactCompilerImpl({
|
||||
filename,
|
||||
userOpts,
|
||||
flowSuppressions: [],
|
||||
unusedOptOutDirectives: [],
|
||||
events: [],
|
||||
};
|
||||
const userLogger: Logger | null = options.logger;
|
||||
@@ -143,11 +173,37 @@ function runReactCompilerImpl({
|
||||
filename,
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
plugins: [
|
||||
[PluginProposalPrivateMethods, {loose: true}],
|
||||
[BabelPluginReactCompiler, options],
|
||||
],
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
});
|
||||
|
||||
if (results.events.filter(e => e.kind === 'CompileError').length === 0) {
|
||||
traverse(babelAST, {
|
||||
FunctionDeclaration(path) {
|
||||
path.node;
|
||||
results.unusedOptOutDirectives.push(
|
||||
...filterUnusedOptOutDirectives(path.node.body.directives),
|
||||
);
|
||||
},
|
||||
ArrowFunctionExpression(path) {
|
||||
if (path.node.body.type === 'BlockStatement') {
|
||||
results.unusedOptOutDirectives.push(
|
||||
...filterUnusedOptOutDirectives(path.node.body.directives),
|
||||
);
|
||||
}
|
||||
},
|
||||
FunctionExpression(path) {
|
||||
results.unusedOptOutDirectives.push(
|
||||
...filterUnusedOptOutDirectives(path.node.body.directives),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
/* errors handled by injected logger */
|
||||
}
|
||||
|
||||
@@ -12,11 +12,10 @@ export default defineConfig({
|
||||
outDir: './dist',
|
||||
external: [
|
||||
'@babel/core',
|
||||
'@babel/plugin-proposal-private-methods',
|
||||
'hermes-parser',
|
||||
'zod',
|
||||
'zod/v4',
|
||||
'zod-validation-error',
|
||||
'zod-validation-error/v4',
|
||||
],
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"fast-glob": "^3.3.2",
|
||||
"ora": "5.4.1",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"engines": {
|
||||
|
||||
@@ -18,9 +18,7 @@ export default defineConfig({
|
||||
'ora',
|
||||
'yargs',
|
||||
'zod',
|
||||
'zod/v4',
|
||||
'zod-validation-error',
|
||||
'zod-validation-error/v4',
|
||||
],
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"html-to-text": "^9.0.5",
|
||||
"prettier": "^3.3.3",
|
||||
"puppeteer": "^24.7.2",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {z} from 'zod/v4';
|
||||
import {z} from 'zod';
|
||||
import {compile, type PrintedCompilerPipelineValue} from './compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
|
||||
@@ -37,9 +37,7 @@
|
||||
"react": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"readline": "^1.3.0",
|
||||
"yargs": "^17.7.1",
|
||||
"zod": "^3.25.0 || ^4.0.0",
|
||||
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
||||
"yargs": "^17.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.1",
|
||||
|
||||
@@ -9,8 +9,8 @@ import {render} from '@testing-library/react';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import React, {MutableRefObject} from 'react';
|
||||
import util from 'util';
|
||||
import {z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {initFbt, toJSON} from './shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,7 @@ async function main() {
|
||||
.option('tag', {
|
||||
description: 'Tag to publish to npm',
|
||||
type: 'choices',
|
||||
choices: ['experimental', 'beta', 'rc', 'latest'],
|
||||
choices: ['experimental', 'beta', 'rc'],
|
||||
default: 'experimental',
|
||||
})
|
||||
.option('tag-version', {
|
||||
@@ -145,15 +145,10 @@ async function main() {
|
||||
files: {exclude: ['.DS_Store']},
|
||||
});
|
||||
const truncatedHash = hash.slice(0, 7);
|
||||
let newVersion;
|
||||
if (argv.tag === 'latest') {
|
||||
newVersion = argv.versionName;
|
||||
} else {
|
||||
newVersion =
|
||||
argv.tagVersion == null || argv.tagVersion === ''
|
||||
? `${argv.versionName}-${argv.tag}`
|
||||
: `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
|
||||
}
|
||||
let newVersion =
|
||||
argv.tagVersion == null || argv.tagVersion === ''
|
||||
? `${argv.versionName}-${argv.tag}`
|
||||
: `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
|
||||
if (argv.tag === 'experimental' || argv.tag === 'beta') {
|
||||
newVersion = `${newVersion}-${truncatedHash}-${dateString}`;
|
||||
}
|
||||
@@ -186,9 +181,21 @@ async function main() {
|
||||
if (otp != null) {
|
||||
opts.push(`--otp=${otp}`);
|
||||
}
|
||||
|
||||
opts.push(`--tag=${argv.tag}`);
|
||||
|
||||
/**
|
||||
* Typically, the `latest` tag is reserved for stable package versions. Since the the compiler
|
||||
* is still pre-release, until we have a stable release let's only add the
|
||||
* `latest` tag to non-experimental releases.
|
||||
*
|
||||
* `latest` is added by default, so we only override it for experimental releases so that
|
||||
* those don't get the `latest` tag.
|
||||
*
|
||||
* TODO: Update this when we have a stable release.
|
||||
*/
|
||||
if (argv.tag === 'experimental') {
|
||||
opts.push('--tag=experimental');
|
||||
} else {
|
||||
opts.push('--tag=latest');
|
||||
}
|
||||
try {
|
||||
await spawnHelper(
|
||||
'npm',
|
||||
|
||||
@@ -326,7 +326,7 @@
|
||||
lru-cache "^5.1.1"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-create-class-features-plugin@^7.25.9", "@babel/helper-create-class-features-plugin@^7.27.0":
|
||||
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.25.9", "@babel/helper-create-class-features-plugin@^7.27.0":
|
||||
version "7.27.0"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz"
|
||||
integrity sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==
|
||||
@@ -706,6 +706,14 @@
|
||||
"@babel/helper-plugin-utils" "^7.25.9"
|
||||
"@babel/traverse" "^7.25.9"
|
||||
|
||||
"@babel/plugin-proposal-private-methods@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz"
|
||||
integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin" "^7.18.6"
|
||||
"@babel/helper-plugin-utils" "^7.18.6"
|
||||
|
||||
"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2":
|
||||
version "7.21.0-placeholder-for-preset-env.2"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz"
|
||||
@@ -10486,7 +10494,16 @@ string-length@^4.0.1:
|
||||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -10559,7 +10576,14 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -11336,7 +11360,7 @@ workerpool@^6.5.1:
|
||||
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
|
||||
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -11354,6 +11378,15 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||
@@ -11505,17 +11538,17 @@ zod-to-json-schema@^3.24.1:
|
||||
resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz"
|
||||
integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
|
||||
|
||||
"zod-validation-error@^3.5.0 || ^4.0.0":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
|
||||
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
|
||||
zod-validation-error@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-2.1.0.tgz"
|
||||
integrity sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==
|
||||
|
||||
zod@^3.23.8, zod@^3.24.1:
|
||||
zod-validation-error@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.0.3.tgz"
|
||||
integrity sha512-cETTrcMq3Ze58vhdR0zD37uJm/694I6mAxcf/ei5bl89cC++fBNxrC2z8lkFze/8hVMPwrbtrwXHR2LB50fpHw==
|
||||
|
||||
zod@^3.22.4, zod@^3.23.8, zod@^3.24.1:
|
||||
version "3.24.3"
|
||||
resolved "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz"
|
||||
integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==
|
||||
|
||||
"zod@^3.25.0 || ^4.0.0":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0"
|
||||
integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==
|
||||
|
||||
@@ -55,11 +55,11 @@ export default function ScrollIntoViewCase() {
|
||||
const scrollContainerRef = useRef(null);
|
||||
|
||||
const scrollVertical = () => {
|
||||
fragmentRef.current.scrollIntoView(alignToTop);
|
||||
fragmentRef.current.experimental_scrollIntoView(alignToTop);
|
||||
};
|
||||
|
||||
const scrollVerticalNoChildren = () => {
|
||||
noChildRef.current.scrollIntoView(alignToTop);
|
||||
noChildRef.current.experimental_scrollIntoView(alignToTop);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user