payload icon indicating copy to clipboard operation
payload copied to clipboard

`payload generate:importmap` Fails with Custom Lexical Editor Feature

Open Tanish2002 opened this issue 1 year ago • 8 comments

Describe the Bug

I've implemented a custom feature for the Lexical editor that allows marking text with colors(just add a tailwind class). This feature is heavily inspired by the LinkFeature code used by payload.

While the feature is nearly complete, I encounter an error when running the command payload generate:importmap. Below is the error output:

❯ pnpm payload generate:importmap                                                                                                                                                                            took 4m12s 

> [email protected] payload /home/weeb/code/javascript/custom-color-feature-repro
> cross-env NODE_OPTIONS=--no-deprecation payload "generate:importmap"


node:internal/process/promises:391
    triggerUncaughtException(err, true /* fromPromise */);
    ^
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" for /home/weeb/code/javascript/custom-color-feature-repro/node_modules/.pnpm/[email protected][email protected]/node_modules/react-image-crop/dist/ReactCrop.css
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:176:9)
    at defaultGetFormat (node:internal/modules/esm/get_format:219:36)
    at defaultLoad (node:internal/modules/esm/load:143:22)
    at async nextLoad (node:internal/modules/esm/hooks:868:22)
    at async load (file:///home/weeb/code/javascript/custom-color-feature-repro/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/esm/index.mjs?1734331094830:2:1762)
    at async nextLoad (node:internal/modules/esm/hooks:868:22)
    at async Hooks.load (node:internal/modules/esm/hooks:451:20)
    at async handleMessage (node:internal/modules/esm/worker:196:18) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

Node.js v20.18.1
 ELIFECYCLE  Command failed with exit code 1.

My Own Observations

Through testing, I narrowed the issue to the createNode method in the server-side feature code. The problem arises specifically with the nodes array that includes the following two custom nodes:

  • AutoColorTextNode
  • ColorTextNode

If I remove the nodes array from the createServerFeature implementation, the payload generate:importmap command works correctly without any errors.


If I remove the nodes array, run payload generate:importmap then run the dev server pnpm run dev and then simply re-add the nodes array it somehow automatically works, while the payload generate:importmap still fails. Below is an image of it working in the admin panel: Image

Since it works using the above method, I can't figure out why this is happening,

Link to the code that reproduces this issue

https://github.com/Tanish2002/custom-color-feature-repro

Reproduction Steps

Error Scenario

  1. Clone the reproduction repository:
git clone <repository-link>
  1. Navigate to the cloned repository and download dependencies:
cd custom-color-feature-repro
pnpm install
  1. Run the following command:
pnpm payload generate:importmap

4.Observe the error output, which matches the following:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" ...

Workaround

To bypass the issue and proceed with development:

  1. Clone the reproduction repository and install dependencies (if not already done):
git clone https://github.com/Tanish2002/custom-color-feature-repro
cd custom-color-feature-repro
pnpm install
  1. Open the file src/features/colorText/server/index.ts.
  2. Locate the createServerFeature function and remove the nodes array from the returned object. For example, change:
return {
  ...
  nodes: [
    createNode({ ... }),
    createNode({ ... }),
  ],
  ...
}

To:

return {
  ...
  // nodes array removed
}
  1. Run the following commands:
pnpm payload generate:importmap
pnpm run dev
  1. After the app starts, re-add the nodes array in createServerFeature to restore its functionality.
  2. Allow the application to hot-reload and verify the feature is working as expected in the admin panel.

If you re-run the pnpm payload generate:importmap command after re-adding the nodes array, the same error occurs.

Which area(s) are affected? (Select all that apply)

plugin: other, plugin: richtext-lexical

Environment Info

Binaries:
  Node: 20.18.1
  npm: 10.8.2
  Yarn: N/A
  pnpm: 9.15.0
Relevant Packages:
  payload: 3.7.0
  next: 15.1.0
  @payloadcms/email-nodemailer: 3.7.0
  @payloadcms/graphql: 3.7.0
  @payloadcms/next/utilities: 3.7.0
  @payloadcms/payload-cloud: 3.7.0
  @payloadcms/richtext-lexical: 3.7.0
  @payloadcms/translations: 3.7.0
  @payloadcms/ui/shared: 3.7.0
  react: 19.0.0
  react-dom: 19.0.0
Operating System:
  Platform: linux
  Arch: x64
  Version: #1-NixOS SMP PREEMPT_DYNAMIC Fri Nov 22 14:38:37 UTC 2024
  Available memory (MB): 15729
  Available CPU cores: 12

Tanish2002 avatar Dec 16 '24 07:12 Tanish2002

That error usually implies there is something wrong with your path (in this case, ClientFeature in createServerFeature).

However I've only seen it in collection level imports, not custom lexical features so I could be wrong.

One thing I can see is that your client feature is using an index.tsx, whereas the example is using index.ts. Could be nothing but worth a shot.

rilrom avatar Dec 16 '24 07:12 rilrom

That error usually implies there is something wrong with your path (in this case, ClientFeature in createServerFeature).

I've tried all types of path formats, converting the import to a default import using #default or #ComponentName, including full paths with index.tsx#ComponentName/default

One thing I can see is that your client feature is using an index.tsx, whereas the example is using index.ts. Could be nothing but worth a shot.

Tried that, didn't work as well, the only reason I'm using .tsx is because of the ChildComponent property is supposed to be a React Component

Tanish2002 avatar Dec 16 '24 07:12 Tanish2002

@rilrom One more thing, I don't think this is even caused by the ClientFeature Import, Like I previously mentioned, the error goes away if I remove the nodes array while the ClientFeature import is still there.

If I remove the nodes array and then run payload generate:importmap it works fine which I can even confirm in the generated importMap.js has the ClientFeature mapped properly.

So I believe the issue lies somewhere in nodes array 🤔. However not confirmed

Tanish2002 avatar Dec 16 '24 08:12 Tanish2002

So it looks to be specific to how you have implemented your custom nodes.

If I replace your custom node with the below:

export class ColorTextNode extends ElementNode {
  static getType(): string {
    return 'colorText'
  }
}

Then importmap works as expected.

I recommend slowly working your way through one of your custom nodes until you find the cause.

rilrom avatar Dec 16 '24 10:12 rilrom

Thanks, @rilrom!

I’ve identified the source of the bug in the ColorTextNode implementation, specifically in the following function:

function $convertAnchorElement(domNode: Node): DOMConversionOutput {
  let node: ColorTextNode | null = null;
  if (isHTMLElement(domNode)) {
    const content = domNode.textContent;
    if (content !== null && content !== '') {
      node = $createColorTextNode({
        id: new ObjectID().toHexString(),
        fields: {
          textColor: domNode.className,
        },
      });
    }
  }
  return { node };
}

The issue seems to be with the isHTMLElement(domNode) check. This utility is imported from @payloadcms/richtext-lexical/client and is also used in their LinkFeature implementation (via isHTMLAnchorElement, which internally uses isHTMLElement). However, I’m puzzled about why this is causing the problem in this context.

As a workaround, I’ve written a custom helper function to specifically check for span elements, which resolves the issue for now:

function $convertAnchorElement(domNode: Node): DOMConversionOutput {
  if (isSpanElement(domNode)) {
    const content = domNode.textContent;
    if (content) {
      const node = $createColorTextNode({
        id: new ObjectID().toHexString(),
        fields: {
          textColor: domNode.className,
        },
      });
      return { node };
    }
  }
  return { node: null };
}

function isSpanElement(domNode: Node): domNode is HTMLSpanElement {
  return domNode.nodeType === 1 && (domNode as HTMLElement).tagName.toLowerCase() === "span";
}

Would appreciate any insights into why isHTMLElement is behaving this way.. So gonna keep this issue open for now as this might be a bug either with the lexical package or payload 🤔

Tanish2002 avatar Dec 16 '24 11:12 Tanish2002

Glad you've found a workaround!

Could you try importing it from '@payloadcms/richtext-lexical/lexical/utils' instead?

It seems the isHTMLElement that you are referencing is different to the one used in the link feature.

rilrom avatar Dec 16 '24 12:12 rilrom

That works! But why? It's the exact same export and here

Tanish2002 avatar Dec 16 '24 12:12 Tanish2002

It seems the isHTMLElement that you are referencing is different to the one used in the link feature.

Yeah I just realized that as well, but that still doesn't explain the issue with importMap command

Tanish2002 avatar Dec 16 '24 12:12 Tanish2002

Hey @Tanish2002 I can shine some light on this. You are importing a CSS file in server-only code, which is crashing the generate:importmap command (because that is a Node command and doesn't know how to parse CSS).

This is why all of our CSS for Lexical features is abstracted out of the server-side files and is present only in the files meant to be consumed solely in a browser context. Browser code goes through Turbopack / Webpack, but the config itself (and the server-side part of Lexical components) is Node-only, so there, CSS will break the process.

If you have a barrel file, or a single file that imports / exports many things (including a CSS file), this would crash the Node scripts that Payload uses. The @payloadcms/richtext-lexical/client export of ours is meant to be imported only in client-side code - not Node code. That's why if you import that utility directly, it doesn't crash, but if you import from our client-side export, other CSS in that file will cause it to crash.

Does that make sense? I'll convert this to a discussion because I think that it will be beneficial for others. Happy to continue to help as well.

jmikrut avatar Dec 17 '24 01:12 jmikrut