feat(richtext-lexical): add editorConfigFactory helper to streamline getting the editor config (#11467)
This PR exports a new `editorConfigFactory` that provides multiple standardized ways to retrieve the editor configuration needed for the Lexical editor. ## Why this is needed Getting the editor config is required for converting the lexical editor state into/from different formats, as it's needed to create a headless editor. While we're moving away from requiring headless editor instantiation for common format conversions, some conversion types and other use cases still require it. Currently, retrieving the editor config is cumbersome - you either need an existing field to extract it from or the payload config to create it from scratch, with multiple approaches for each method. ## What this PR does The `editorConfigFactory` consolidates all possible ways to retrieve the editor config into a single factory with clear methods: ```ts editorConfigFactory.default() editorConfigFactory.fromField() editorConfigFactory.fromUnsanitizedField() editorConfigFactory.fromFeatures() editorConfigFactory.fromEditor() ``` This results in less code, simpler implementation, and improved developer experience. The PR also adds documentation for all retrieval methods.
This commit is contained in:
@@ -346,48 +346,73 @@ A headless editor can perform such conversions outside of the main editor instan
|
||||
|
||||
```ts
|
||||
import { createHeadlessEditor } from '@payloadcms/richtext-lexical/lexical/headless'
|
||||
import { getEnabledNodes, sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
import { getEnabledNodes, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const yourEditorConfig // <= your editor config here
|
||||
const payloadConfig // <= your Payload Config here
|
||||
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig: sanitizeServerEditorConfig(yourEditorConfig, payloadConfig),
|
||||
editorConfig: await editorConfigFactory.default({config: payloadConfig})
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
### Getting the editor config
|
||||
|
||||
As you can see, you need to provide an editor config in order to create a headless editor. This is because the editor config is used to determine which nodes & features are enabled, and which converters are used.
|
||||
You need to provide an editor config in order to create a headless editor. This is because the editor config is used to determine which nodes & features are enabled, and which converters are used.
|
||||
|
||||
To get the editor config, simply import the default editor config and adjust it - just like you did inside of the `editor: lexicalEditor({})` property:
|
||||
To get the editor config, import the `editorConfigFactory` factory - this factory provides a variety of ways to get the editor config, depending on your use case.
|
||||
|
||||
```ts
|
||||
import { defaultEditorConfig, defaultEditorFeatures } from '@payloadcms/richtext-lexical' // <= make sure this package is installed
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
const yourEditorConfig = defaultEditorConfig
|
||||
import {
|
||||
editorConfigFactory,
|
||||
FixedToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
// If you made changes to the features of the field's editor config, you should also make those changes here:
|
||||
yourEditorConfig.features = [
|
||||
...defaultEditorFeatures,
|
||||
// Add your custom features here
|
||||
]
|
||||
// Your config needs to be available in order to retrieve the default editor config
|
||||
const config: SanitizedConfig = {} as SanitizedConfig
|
||||
|
||||
// Version 1 - use the default editor config
|
||||
const yourEditorConfig = await editorConfigFactory.default({ config })
|
||||
|
||||
// Version 2 - if you have access to a lexical fields, you can extract the editor config from it
|
||||
const yourEditorConfig2 = editorConfigFactory.fromField({
|
||||
field: collectionConfig.fields[1],
|
||||
})
|
||||
|
||||
// Version 3 - create a new editor config - behaves just like instantiating a new `lexicalEditor`
|
||||
const yourEditorConfig3 = await editorConfigFactory.fromFeatures({
|
||||
config,
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures, FixedToolbarFeature()],
|
||||
})
|
||||
|
||||
// Version 4 - if you have instantiated a lexical editor and are accessing it outside a field (=> this is the unsanitized editor),
|
||||
// you can extract the editor config from it.
|
||||
// This is common if you define the editor in a re-usable module scope variable and pass it to the richText field.
|
||||
// This is the least efficient way to get the editor config, and not recommended. It is recommended to extract the `features` arg
|
||||
// into a separate variable and use `fromFeatures` instead.
|
||||
const editor = lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures, FixedToolbarFeature()],
|
||||
})
|
||||
|
||||
const yourEditorConfig4 = await editorConfigFactory.fromEditor({
|
||||
config,
|
||||
editor,
|
||||
})
|
||||
```
|
||||
|
||||
### Getting the editor config from an existing field
|
||||
### Example - Getting the editor config from an existing field
|
||||
|
||||
If you have access to the sanitized collection config, you can get access to the lexical sanitized editor config & features, as every lexical richText field returns it. Here is an example how you can get it from another field's afterRead hook:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig, RichTextField } from 'payload'
|
||||
|
||||
import { editorConfigFactory, getEnabledNodes, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { createHeadlessEditor } from '@payloadcms/richtext-lexical/lexical/headless'
|
||||
import type { LexicalRichTextAdapter, SanitizedServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
getEnabledNodes,
|
||||
lexicalEditor
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
slug: 'slug',
|
||||
@@ -397,20 +422,18 @@ export const MyCollection: CollectionConfig = {
|
||||
type: 'text',
|
||||
hooks: {
|
||||
afterRead: [
|
||||
({ value, collection }) => {
|
||||
const otherRichTextField: RichTextField = collection.fields.find(
|
||||
({ siblingFields, value }) => {
|
||||
const field: RichTextField = siblingFields.find(
|
||||
(field) => 'name' in field && field.name === 'richText',
|
||||
) as RichTextField
|
||||
|
||||
const lexicalAdapter: LexicalRichTextAdapter =
|
||||
otherRichTextField.editor as LexicalRichTextAdapter
|
||||
|
||||
const sanitizedServerEditorConfig: SanitizedServerEditorConfig =
|
||||
lexicalAdapter.editorConfig
|
||||
const editorConfig = editorConfigFactory.fromField({
|
||||
field,
|
||||
})
|
||||
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig: sanitizedServerEditorConfig,
|
||||
editorConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -424,11 +447,9 @@ export const MyCollection: CollectionConfig = {
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features,
|
||||
}),
|
||||
}
|
||||
]
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -479,14 +500,14 @@ This has been taken from the [lexical serialization & deserialization docs](http
|
||||
Convert markdown content to the Lexical editor format with the following:
|
||||
|
||||
```ts
|
||||
import { sanitizeServerEditorConfig, $convertFromMarkdownString } from '@payloadcms/richtext-lexical'
|
||||
import { $convertFromMarkdownString, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & Payload Config here
|
||||
const yourEditorConfig = await editorConfigFactory.default({ config })
|
||||
const markdown = `# Hello World`
|
||||
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
$convertFromMarkdownString(markdown, yourSanitizedEditorConfig.features.markdownTransformers)
|
||||
$convertFromMarkdownString(markdown, yourEditorConfig.features.markdownTransformers)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
@@ -505,11 +526,12 @@ Export content from the Lexical editor into Markdown format using these steps:
|
||||
Here's the code for it:
|
||||
|
||||
```ts
|
||||
import { $convertToMarkdownString } from '@payloadcms/richtext-lexical/lexical/markdown'
|
||||
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & Payload Config here
|
||||
import { editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
import { $convertToMarkdownString } from '@payloadcms/richtext-lexical/lexical/markdown'
|
||||
|
||||
const yourEditorConfig = await editorConfigFactory.default({ config })
|
||||
const yourEditorState: SerializedEditorState // <= your current editor state here
|
||||
|
||||
// Import editor state into your headless editor
|
||||
@@ -527,7 +549,7 @@ try {
|
||||
// Export to markdown
|
||||
let markdown: string
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
markdown = $convertToMarkdownString(yourSanitizedEditorConfig?.features?.markdownTransformers)
|
||||
markdown = $convertToMarkdownString(yourEditorConfig?.features?.markdownTransformers)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -542,6 +564,7 @@ Here's the code for it:
|
||||
|
||||
```ts
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { $getRoot } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
const yourEditorState: SerializedEditorState // <= your current editor state here
|
||||
|
||||
@@ -12,19 +12,13 @@ import {
|
||||
|
||||
import type { FeatureProviderServer, ResolvedServerFeatureMap } from './features/typesServer.js'
|
||||
import type { SanitizedServerEditorConfig } from './lexical/config/types.js'
|
||||
import type {
|
||||
AdapterProps,
|
||||
LexicalEditorProps,
|
||||
LexicalRichTextAdapter,
|
||||
LexicalRichTextAdapterProvider,
|
||||
} from './types.js'
|
||||
import type { AdapterProps, LexicalEditorProps, LexicalRichTextAdapterProvider } from './types.js'
|
||||
|
||||
import { getDefaultSanitizedEditorConfig } from './getDefaultSanitizedEditorConfig.js'
|
||||
import { i18n } from './i18n.js'
|
||||
import { defaultEditorConfig, defaultEditorFeatures } from './lexical/config/server/default.js'
|
||||
import { loadFeatures } from './lexical/config/server/loader.js'
|
||||
import { sanitizeServerFeatures } from './lexical/config/server/sanitize.js'
|
||||
import { defaultEditorFeatures } from './lexical/config/server/default.js'
|
||||
import { populateLexicalPopulationPromises } from './populateGraphQL/populateLexicalPopulationPromises.js'
|
||||
import { featuresInputToEditorConfig } from './utilities/editorConfigFactory.js'
|
||||
import { getGenerateImportMap } from './utilities/generateImportMap.js'
|
||||
import { getGenerateSchemaMap } from './utilities/generateSchemaMap.js'
|
||||
import { recurseNodeTree } from './utilities/recurseNodeTree.js'
|
||||
@@ -34,7 +28,7 @@ let checkedDependencies = false
|
||||
|
||||
export const lexicalTargetVersion = '0.21.0'
|
||||
|
||||
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider {
|
||||
export function lexicalEditor(args?: LexicalEditorProps): LexicalRichTextAdapterProvider {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true' &&
|
||||
@@ -66,7 +60,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
let resolvedFeatureMap: ResolvedServerFeatureMap
|
||||
|
||||
let finalSanitizedEditorConfig: SanitizedServerEditorConfig // For server only
|
||||
if (!props || (!props.features && !props.lexical)) {
|
||||
if (!args || (!args.features && !args.lexical)) {
|
||||
finalSanitizedEditorConfig = await getDefaultSanitizedEditorConfig({
|
||||
config,
|
||||
parentIsLocalized,
|
||||
@@ -76,41 +70,16 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
|
||||
resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap
|
||||
} else {
|
||||
if (props.features && typeof props.features === 'function') {
|
||||
const rootEditor = config.editor
|
||||
let rootEditorFeatures: FeatureProviderServer<unknown, unknown, unknown>[] = []
|
||||
if (typeof rootEditor === 'object' && 'features' in rootEditor) {
|
||||
rootEditorFeatures = (rootEditor as LexicalRichTextAdapter).features
|
||||
}
|
||||
features = props.features({
|
||||
defaultFeatures: defaultEditorFeatures,
|
||||
rootFeatures: rootEditorFeatures,
|
||||
})
|
||||
} else {
|
||||
features = props.features as FeatureProviderServer<unknown, unknown, unknown>[]
|
||||
}
|
||||
|
||||
if (!features) {
|
||||
features = defaultEditorFeatures
|
||||
}
|
||||
|
||||
const lexical = props.lexical ?? defaultEditorConfig.lexical
|
||||
|
||||
resolvedFeatureMap = await loadFeatures({
|
||||
const result = await featuresInputToEditorConfig({
|
||||
config,
|
||||
features: args?.features,
|
||||
isRoot,
|
||||
lexical: args?.lexical,
|
||||
parentIsLocalized,
|
||||
unSanitizedEditorConfig: {
|
||||
features,
|
||||
lexical,
|
||||
},
|
||||
})
|
||||
|
||||
finalSanitizedEditorConfig = {
|
||||
features: sanitizeServerFeatures(resolvedFeatureMap),
|
||||
lexical: props.lexical,
|
||||
resolvedFeatureMap,
|
||||
}
|
||||
finalSanitizedEditorConfig = result.sanitizedConfig
|
||||
features = result.features
|
||||
resolvedFeatureMap = result.resolvedFeatureMap
|
||||
}
|
||||
|
||||
const featureI18n = finalSanitizedEditorConfig.features.i18n
|
||||
@@ -128,7 +97,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
CellComponent: {
|
||||
path: '@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell',
|
||||
serverProps: {
|
||||
admin: props?.admin,
|
||||
admin: args?.admin,
|
||||
sanitizedEditorConfig: finalSanitizedEditorConfig,
|
||||
},
|
||||
},
|
||||
@@ -137,7 +106,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
FieldComponent: {
|
||||
path: '@payloadcms/richtext-lexical/rsc#RscEntryLexicalField',
|
||||
serverProps: {
|
||||
admin: props?.admin,
|
||||
admin: args?.admin,
|
||||
sanitizedEditorConfig: finalSanitizedEditorConfig,
|
||||
},
|
||||
},
|
||||
@@ -1055,6 +1024,7 @@ export type { LexicalEditorProps, LexicalFieldAdminProps, LexicalRichTextAdapter
|
||||
|
||||
export { createServerFeature } from './utilities/createServerFeature.js'
|
||||
|
||||
export { editorConfigFactory } from './utilities/editorConfigFactory.js'
|
||||
export type { FieldsDrawerProps } from './utilities/fieldsDrawer/Drawer.js'
|
||||
export { extractPropsFromJSXPropsString } from './utilities/jsx/extractPropsFromJSXPropsString.js'
|
||||
export {
|
||||
@@ -1063,4 +1033,5 @@ export {
|
||||
objectToFrontmatter,
|
||||
propsToJSXString,
|
||||
} from './utilities/jsx/jsx.js'
|
||||
|
||||
export { upgradeLexicalData } from './utilities/upgradeLexicalData/index.js'
|
||||
|
||||
@@ -38,9 +38,7 @@ export type LexicalFieldAdminClientProps = {
|
||||
placeholder?: string
|
||||
} & Omit<LexicalFieldAdminProps, 'placeholder'>
|
||||
|
||||
export type LexicalEditorProps = {
|
||||
admin?: LexicalFieldAdminProps
|
||||
features?:
|
||||
export type FeaturesInput =
|
||||
| (({
|
||||
defaultFeatures,
|
||||
rootFeatures,
|
||||
@@ -72,6 +70,10 @@ export type LexicalEditorProps = {
|
||||
rootFeatures: FeatureProviderServer<any, any, any>[]
|
||||
}) => FeatureProviderServer<any, any, any>[])
|
||||
| FeatureProviderServer<any, any, any>[]
|
||||
|
||||
export type LexicalEditorProps = {
|
||||
admin?: LexicalFieldAdminProps
|
||||
features?: FeaturesInput
|
||||
lexical?: LexicalEditorConfig
|
||||
}
|
||||
|
||||
|
||||
140
packages/richtext-lexical/src/utilities/editorConfigFactory.ts
Normal file
140
packages/richtext-lexical/src/utilities/editorConfigFactory.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical'
|
||||
import type { RichTextAdapterProvider, RichTextField, SanitizedConfig } from 'payload'
|
||||
|
||||
import type { FeatureProviderServer, ResolvedServerFeatureMap } from '../features/typesServer.js'
|
||||
import type { SanitizedServerEditorConfig } from '../lexical/config/types.js'
|
||||
import type {
|
||||
FeaturesInput,
|
||||
LexicalRichTextAdapter,
|
||||
LexicalRichTextAdapterProvider,
|
||||
} from '../types.js'
|
||||
|
||||
import { getDefaultSanitizedEditorConfig } from '../getDefaultSanitizedEditorConfig.js'
|
||||
import { defaultEditorConfig, defaultEditorFeatures } from '../lexical/config/server/default.js'
|
||||
import { loadFeatures } from '../lexical/config/server/loader.js'
|
||||
import { sanitizeServerFeatures } from '../lexical/config/server/sanitize.js'
|
||||
|
||||
export const editorConfigFactory = {
|
||||
default: async (args: {
|
||||
config: SanitizedConfig
|
||||
parentIsLocalized?: boolean
|
||||
}): Promise<SanitizedServerEditorConfig> => {
|
||||
return getDefaultSanitizedEditorConfig({
|
||||
config: args.config,
|
||||
parentIsLocalized: args.parentIsLocalized ?? false,
|
||||
})
|
||||
},
|
||||
/**
|
||||
* If you have instantiated a lexical editor and are accessing it outside a field (=> this is the unsanitized editor),
|
||||
* you can extract the editor config from it.
|
||||
* This is common if you define the editor in a re-usable module scope variable and pass it to the richText field.
|
||||
*
|
||||
* This is the least efficient way to get the editor config, and not recommended. It is recommended to extract the `features` arg
|
||||
* into a separate variable and use `fromFeatures` instead.
|
||||
*/
|
||||
fromEditor: async (args: {
|
||||
config: SanitizedConfig
|
||||
editor: LexicalRichTextAdapterProvider
|
||||
isRoot?: boolean
|
||||
lexical?: LexicalEditorConfig
|
||||
parentIsLocalized?: boolean
|
||||
}): Promise<SanitizedServerEditorConfig> => {
|
||||
const lexicalAdapter: LexicalRichTextAdapter = await args.editor({
|
||||
config: args.config,
|
||||
isRoot: args.isRoot ?? false,
|
||||
parentIsLocalized: args.parentIsLocalized ?? false,
|
||||
})
|
||||
|
||||
const sanitizedServerEditorConfig: SanitizedServerEditorConfig = lexicalAdapter.editorConfig
|
||||
return sanitizedServerEditorConfig
|
||||
},
|
||||
/**
|
||||
* Create a new editor config - behaves just like instantiating a new `lexicalEditor`
|
||||
*/
|
||||
fromFeatures: async (args: {
|
||||
config: SanitizedConfig
|
||||
features?: FeaturesInput
|
||||
isRoot?: boolean
|
||||
lexical?: LexicalEditorConfig
|
||||
parentIsLocalized?: boolean
|
||||
}): Promise<SanitizedServerEditorConfig> => {
|
||||
return (await featuresInputToEditorConfig(args)).sanitizedConfig
|
||||
},
|
||||
fromField: (args: { field: RichTextField }): SanitizedServerEditorConfig => {
|
||||
const lexicalAdapter: LexicalRichTextAdapter = args.field.editor as LexicalRichTextAdapter
|
||||
|
||||
const sanitizedServerEditorConfig: SanitizedServerEditorConfig = lexicalAdapter.editorConfig
|
||||
return sanitizedServerEditorConfig
|
||||
},
|
||||
fromUnsanitizedField: async (args: {
|
||||
config: SanitizedConfig
|
||||
field: RichTextField
|
||||
isRoot?: boolean
|
||||
parentIsLocalized?: boolean
|
||||
}): Promise<SanitizedServerEditorConfig> => {
|
||||
const lexicalAdapterProvider: RichTextAdapterProvider = args.field
|
||||
.editor as RichTextAdapterProvider
|
||||
|
||||
const lexicalAdapter: LexicalRichTextAdapter = (await lexicalAdapterProvider({
|
||||
config: args.config,
|
||||
isRoot: args.isRoot ?? false,
|
||||
parentIsLocalized: args.parentIsLocalized ?? false,
|
||||
})) as LexicalRichTextAdapter
|
||||
|
||||
const sanitizedServerEditorConfig: SanitizedServerEditorConfig = lexicalAdapter.editorConfig
|
||||
return sanitizedServerEditorConfig
|
||||
},
|
||||
}
|
||||
|
||||
export const featuresInputToEditorConfig = async (args: {
|
||||
config: SanitizedConfig
|
||||
features?: FeaturesInput
|
||||
isRoot?: boolean
|
||||
lexical?: LexicalEditorConfig
|
||||
parentIsLocalized?: boolean
|
||||
}): Promise<{
|
||||
features: FeatureProviderServer<unknown, unknown, unknown>[]
|
||||
resolvedFeatureMap: ResolvedServerFeatureMap
|
||||
sanitizedConfig: SanitizedServerEditorConfig
|
||||
}> => {
|
||||
let features: FeatureProviderServer<unknown, unknown, unknown>[] = []
|
||||
if (args.features && typeof args.features === 'function') {
|
||||
const rootEditor = args.config.editor
|
||||
let rootEditorFeatures: FeatureProviderServer<unknown, unknown, unknown>[] = []
|
||||
if (typeof rootEditor === 'object' && 'features' in rootEditor) {
|
||||
rootEditorFeatures = (rootEditor as LexicalRichTextAdapter).features
|
||||
}
|
||||
features = args.features({
|
||||
defaultFeatures: defaultEditorFeatures,
|
||||
rootFeatures: rootEditorFeatures,
|
||||
})
|
||||
} else {
|
||||
features = args.features as FeatureProviderServer<unknown, unknown, unknown>[]
|
||||
}
|
||||
|
||||
if (!features) {
|
||||
features = defaultEditorFeatures
|
||||
}
|
||||
|
||||
const lexical = args.lexical ?? defaultEditorConfig.lexical
|
||||
|
||||
const resolvedFeatureMap = await loadFeatures({
|
||||
config: args.config,
|
||||
isRoot: args.isRoot ?? false,
|
||||
parentIsLocalized: args.parentIsLocalized ?? false,
|
||||
unSanitizedEditorConfig: {
|
||||
features,
|
||||
lexical,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
features,
|
||||
resolvedFeatureMap,
|
||||
sanitizedConfig: {
|
||||
features: sanitizeServerFeatures(resolvedFeatureMap),
|
||||
lexical: args.lexical,
|
||||
resolvedFeatureMap,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user