Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 32 additions & 34 deletions compiler/apps/playground/components/Editor/ConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,51 @@
*/

import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {parseConfigPragmaAsString} from 'babel-plugin-react-compiler';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {useState, useEffect} from 'react';
import {useState} from 'react';
import {Resizable} from 're-resizable';
import {useStore} from '../StoreContext';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
import {
generateOverridePragmaFromConfig,
updateSourceWithOverridePragma,
} from '../../lib/configUtils';

loader.config({monaco});

export default function ConfigEditor(): JSX.Element {
const [, setMonaco] = useState<Monaco | null>(null);
const store = useStore();
const dispatchStore = useStoreDispatch();

// Parse string-based override config from pragma comment and format it
const [configJavaScript, setConfigJavaScript] = useState('');
const handleChange: (value: string | undefined) => void = async value => {
if (value === undefined) return;

useEffect(() => {
const pragma = store.source.substring(0, store.source.indexOf('\n'));
const configString = `(${parseConfigPragmaAsString(pragma)})`;
try {
const newPragma = await generateOverridePragmaFromConfig(value);
const updatedSource = updateSourceWithOverridePragma(
store.source,
newPragma,
);

prettier
.format(configString, {
semi: true,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
})
.then(formatted => {
setConfigJavaScript(formatted);
})
.catch(error => {
console.error('Error formatting config:', error);
setConfigJavaScript('({})'); // Return empty object if not valid for now
//TODO: Add validation and error handling for config
// Update the store with both the new config and updated source
dispatchStore({
type: 'updateFile',
payload: {
source: updatedSource,
config: value,
},
});
console.log('Config:', configString);
}, [store.source]);

const handleChange: (value: string | undefined) => void = value => {
if (!value) return;

// TODO: Implement sync logic to update pragma comments in the source
console.log('Config changed:', value);
} catch (_) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kinds of errors are we swallowing here? Are these errors due to parsing? Probably we want to display some user feedback.

Seems like the "Apply" button would allow for a better error handling pattern as well. We could reject invalid content with informative messages.

Fine to land this for now since the plan is to follow up with the button sync instead of live sync anyways.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the errors should all be parsing errors

dispatchStore({
type: 'updateFile',
payload: {
source: store.source,
config: value,
},
});
}
};

const handleMount: (
Expand Down Expand Up @@ -81,12 +80,11 @@ export default function ConfigEditor(): JSX.Element {
<MonacoEditor
path={'config.js'}
language={'javascript'}
value={configJavaScript}
value={store.config}
onMount={handleMount}
onChange={handleChange}
options={{
...monacoOptions,
readOnly: true,
lineNumbers: 'off',
folding: false,
renderLineHighlight: 'none',
Expand Down
15 changes: 12 additions & 3 deletions compiler/apps/playground/components/Editor/EditorImpl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
import {useSearchParams} from 'next/navigation';
import {parseAndFormatConfig} from '../../lib/configUtils';

function parseInput(
input: string,
Expand Down Expand Up @@ -315,9 +316,17 @@ export default function Editor(): JSX.Element {
});
mountStore = defaultStore;
}
dispatchStore({
type: 'setStore',
payload: {store: mountStore},

parseAndFormatConfig(mountStore.source).then(config => {
dispatchStore({
type: 'setStore',
payload: {
store: {
...mountStore,
config,
},
},
});
});
});

Expand Down
7 changes: 6 additions & 1 deletion compiler/apps/playground/components/Editor/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
import React$Types from '../../node_modules/@types/react/index.d.ts';
import {parseAndFormatConfig} from '../../lib/configUtils.ts';

loader.config({monaco});

Expand Down Expand Up @@ -79,13 +80,17 @@ export default function Input({errors, language}: Props): JSX.Element {
});
}, [monaco, language]);

const handleChange: (value: string | undefined) => void = value => {
const handleChange: (value: string | undefined) => void = async value => {
if (!value) return;

// Parse and format the config
const config = await parseAndFormatConfig(value);

dispatchStore({
type: 'updateFile',
payload: {
source: value,
config,
},
});
};
Expand Down
4 changes: 3 additions & 1 deletion compiler/apps/playground/components/StoreContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type ReducerAction =
type: 'updateFile';
payload: {
source: string;
config?: string;
};
};

Expand All @@ -66,10 +67,11 @@ function storeReducer(store: Store, action: ReducerAction): Store {
return newStore;
}
case 'updateFile': {
const {source} = action.payload;
const {source, config} = action.payload;
const newStore = {
...store,
source,
config,
};
return newStore;
}
Expand Down
87 changes: 87 additions & 0 deletions compiler/apps/playground/lib/configUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* 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 parserBabel from 'prettier/plugins/babel';
import prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';

/**
* Parse config from pragma and format it with prettier
*/
export async function parseAndFormatConfig(source: string): Promise<string> {
const pragma = source.substring(0, source.indexOf('\n'));
let configString = parseConfigPragmaAsString(pragma);
if (configString !== '') {
configString = `(${configString})`;
}

try {
const formatted = await prettier.format(configString, {
semi: true,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
return formatted;
} catch (error) {
console.error('Error formatting config:', error);
return ''; // Return empty string if not valid for now
}
}

function extractCurlyBracesContent(input: string): string {
const startIndex = input.indexOf('{');
const endIndex = input.lastIndexOf('}');
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
throw new Error('No outer curly braces found in input');
}
return input.slice(startIndex, endIndex + 1);
}

function cleanContent(content: string): string {
return content
.replace(/[\r\n]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}

/**
* Generate a the override pragma comment from a formatted config object string
*/
export async function generateOverridePragmaFromConfig(
formattedConfigString: string,
): Promise<string> {
const content = extractCurlyBracesContent(formattedConfigString);
const cleanConfig = cleanContent(content);

// Format the config to ensure it's valid
await prettier.format(`(${cleanConfig})`, {
semi: false,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});

return `// @OVERRIDE:${cleanConfig}`;
}

/**
* Update the override pragma comment in source code.
*/
export function updateSourceWithOverridePragma(
source: string,
newPragma: string,
): string {
const firstLineEnd = source.indexOf('\n');
const firstLine = source.substring(0, firstLineEnd);

const pragmaRegex = /^\/\/\s*@/;
if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) {
return newPragma + source.substring(firstLineEnd);
} else {
return newPragma + '\n' + source;
}
}
2 changes: 2 additions & 0 deletions compiler/apps/playground/lib/defaultStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export default function MyApp() {

export const defaultStore: Store = {
source: index,
config: '',
};

export const emptyStore: Store = {
source: '',
config: '',
};
10 changes: 10 additions & 0 deletions compiler/apps/playground/lib/stores/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {defaultStore} from '../defaultStore';
*/
export interface Store {
source: string;
config?: string;
}
export function encodeStore(store: Store): string {
return compressToEncodedURIComponent(JSON.stringify(store));
Expand Down Expand Up @@ -65,5 +66,14 @@ export function initStoreFromUrlOrLocalStorage(): Store {
const raw = decodeStore(encodedSource);

invariant(isValidStore(raw), 'Invalid Store');

// Add config property if missing for backwards compatibility
if (!('config' in raw)) {
return {
...raw,
config: '',
};
}

return raw;
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,16 @@ function parseConfigStringAsJS(

console.log('OVERRIDE:', parsedConfig);

const environment = parseConfigPragmaEnvironmentForTest(
'',
defaults.environment ?? {},
);

const options: Record<keyof PluginOptions, unknown> = {
...defaultOptions,
panicThreshold: 'all_errors',
compilationMode: defaults.compilationMode,
environment: defaults.environment ?? defaultOptions.environment,
environment,
};

// Apply parsed config, merging environment if it exists
Expand All @@ -269,22 +274,9 @@ function parseConfigStringAsJS(
...parsedConfig.environment,
};

// Apply complex defaults for environment flags that are set to true
const environmentConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
{};
for (const [key, value] of Object.entries(mergedEnvironment)) {
if (hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
if (value === true && key in testComplexConfigDefaults) {
environmentConfig[key] = testComplexConfigDefaults[key];
} else {
environmentConfig[key] = value;
}
}
}

// Validate environment config
const validatedEnvironment =
EnvironmentConfigSchema.safeParse(environmentConfig);
EnvironmentConfigSchema.safeParse(mergedEnvironment);
if (!validatedEnvironment.success) {
CompilerError.invariant(false, {
reason: 'Invalid environment configuration in config pragma',
Expand All @@ -294,10 +286,6 @@ function parseConfigStringAsJS(
});
}

if (validatedEnvironment.data.enableResetCacheOnSourceFileChanges == null) {
validatedEnvironment.data.enableResetCacheOnSourceFileChanges = false;
}

options.environment = validatedEnvironment.data;
}

Expand All @@ -308,9 +296,7 @@ function parseConfigStringAsJS(
}

if (hasOwnProperty(defaultOptions, key)) {
if (value === true && key in testComplexPluginOptionDefaults) {
options[key] = testComplexPluginOptionDefaults[key];
} else if (key === 'target' && value === 'donotuse_meta_internal') {
if (key === 'target' && value === 'donotuse_meta_internal') {
options[key] = {
kind: value,
runtimeModule: 'react',
Expand Down
5 changes: 1 addition & 4 deletions compiler/packages/babel-plugin-react-compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,7 @@ export {
printReactiveFunction,
printReactiveFunctionWithOutlined,
} from './ReactiveScopes';
export {
parseConfigPragmaForTests,
parseConfigPragmaAsString,
} from './Utils/TestUtils';
export {parseConfigPragmaForTests} from './Utils/TestUtils';
declare global {
let __DEV__: boolean | null | undefined;
}
Expand Down
Loading