epam / ketcher

Web-based molecule sketcher
https://lifescience.opensource.epam.com/ketcher/demo.html
Apache License 2.0
499 stars 173 forks source link

heavy bundle with ketcher #5947

Open hellhorse123 opened 2 weeks ago

hellhorse123 commented 2 weeks ago

Background

When adding libraries:

import { Ketcher, StandaloneStructServiceProvider } from "ketcher-standalone";
import { Editor } from "ketcher-react";
import "ketcher-react/dist/index.css";

my final bundle increases by about 13 MB, which is a lot. I tried to optimize via CDN, but it didn't work because the ketcher-standalone and ketcher-react libraries loaded from CDN use CommonJS syntax (exports, module.exports), which is not supported in browsers. How can i solve this problem?

This is working example of ketcher:

/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from "react";
import { Ketcher, StandaloneStructServiceProvider } from "ketcher-standalone";
import { Editor } from "ketcher-react";
import "ketcher-react/dist/index.css";
import { Button } from "components/UI/button";
import { ModalComponent } from "components/UI/modal/ModalComponent";
import "./style.scss";

// Initialize the structServiceProvider for Ketcher (standalone version)
const structServiceProvider = new StandaloneStructServiceProvider();

interface ModalProps {
  onOk: (smiles: any) => void;
  onCancel: (e: React.MouseEvent) => void;
  defaultSmilesValue: string;
  isModalOpen: boolean;
  isLoading: boolean;
}

export const ModalContentDrawer: React.FC<ModalProps> = ({
  onOk,
  onCancel,
  defaultSmilesValue,
  isModalOpen,
  isLoading,
}) => {
  const [hasError, setHasError] = useState(false);
  const [errorMessage, setErrorMessage] = useState("");
  const [isKetcherInitialized, setIsKetcherInitialized] = useState(false); // Track Ketcher initialization
  const [ketcherInstance, setKetcherInstance] = useState<Ketcher | null>(null); // Store Ketcher instance

  // Используйте useEffect для управления жизненным циклом Ketcher
  useEffect(() => {
    if (
      ketcherInstance &&
      isKetcherInitialized &&
      defaultSmilesValue &&
      isModalOpen
    ) {
      try {
        ketcherInstance.setMolecule(defaultSmilesValue);
      } catch (error) {
        console.error("Failed to set SMILES:", error);
        setHasError(true);
        setErrorMessage("Error setting molecule in Ketcher.");
      }
    }
  }, [defaultSmilesValue, isKetcherInitialized, ketcherInstance, isModalOpen]);

  const handleSubmit = async () => {
    if (!isKetcherInitialized) {
      setErrorMessage("Ketcher is not fully initialized.");
      return;
    }

    if (ketcherInstance) {
      try {
        // Optionally validate molecule before getting SMILES
        const isMoleculeValid = ketcherInstance.getSmiles(); // Ensure there is a valid structure

        if (!isMoleculeValid) {
          throw new Error("Molecule is invalid or empty.");
        }

        const smiles = await ketcherInstance.getSmiles();

        onOk(smiles);
      } catch (error: unknown) {
        const errMessage = (error as Error).message;
        console.error("Error getting SMILES:", errMessage);

        if (errMessage.includes("Unexpected end of JSON input")) {
          setErrorMessage(
            "Error retrieving SMILES: Invalid or incomplete molecular structure."
          );
        } else {
          setErrorMessage("Failed to retrieve SMILES. Please try again.");
        }

        setHasError(true);
      }
    }
  };

  return (
    <ModalComponent
      modalTitle={"Structure"}
      isModalOpen={isModalOpen}
      onOk={onOk}
      onCancel={onCancel}
      width={930}
    >
      <div className="modal-drawer-container">
        <Editor
          buttons={{
            sgroup: { hidden: true },
            images: { hidden: true },
            text: { hidden: true },
          }}
          staticResourcesUrl={process.env.PUBLIC_URL ?? "./"}
          structServiceProvider={structServiceProvider as any}
          errorHandler={(message: string | Error) => {
            console.error("Error in Ketcher:", message);
            setHasError(true);
            setErrorMessage(
              typeof message === "string" ? message : message.toString()
            );
          }}
          onInit={(ketcher: Ketcher) => {
            // Assign Ketcher instance to window for KetcherLogger to use
            window.ketcher = ketcher;
            if (!ketcherInstance) {
              // Проверяем, был ли уже инициализирован Ketcher
              setKetcherInstance(ketcher); // Set Ketcher instance in state
              setIsKetcherInitialized(true); // Mark Ketcher as initialized
              try {
                ketcher.setMolecule(defaultSmilesValue); // Set the default SMILES after initialization
              } catch (error) {
                console.error("Error initializing Ketcher with SMILES:", error);
                setHasError(true);
                setErrorMessage("Error initializing molecule in Ketcher.");
              }
            }
          }}
        />
        {hasError && <div className="error-message">{errorMessage}</div>}
        <div className="modal-footer">
          <Button onClick={handleSubmit} isLoading={isLoading}>
            Confirm
          </Button>
          <Button onClick={onCancel} type="secondary">
            Cancel
          </Button>
        </div>
      </div>
    </ModalComponent>
  );
};