facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
19.9k stars 1.69k forks source link

Feature: Use subpath exports to declare what is and isn't public #5345

Closed mxxk closed 6 months ago

mxxk commented 11 months ago

I'm working on migrating from DraftJS, and am looking to use various plugins from @lexical/react. However, I've noticed that most plugins there are not actually documented, which makes me wonder whether such undocumented plugins (e.g., LexicalContextMenuPlugin) and hooks (e.g., useLexicalTextEntity) are okay to use. Presently, it's not very clear what is part of the package's public interface (and therefore can be safely imported and depended on) and what is part of the package's internals (and therefore should not be imported). The risk of consuming a private part of any package is that it is unsupported and there are no stability guarantees around it.

I imagine that whatever Lexical has included in the documentation is part of the public interface. However, documentation doesn't always keep pace with the pace of rapid development, so I wonder if the Lexical team has considered one of the following approaches to declare the interface of Lexical packages?

  1. Using package.json subpath exports. Subpath exports were introduced in Node.js v12.7.0 (released July 23, 2019), and have gained JS build tool support over the years (Webpack, Rollup, Vite).
  2. Establishing a convention which distinguishes public and private import paths of Lexical packages. An example of such a convention can be seen at Material UI:

    Be aware that we only support first and second-level imports. Anything deeper is considered private and can cause issues, such as module duplication in your bundle.

    
    // ✅ OK
    import { Add as AddIcon } from '@mui/icons-material';
    import { Tabs } from '@mui/material';
    //                         ^^^^^^^^ 1st or top-level
    
    // ✅ OK
    import AddIcon from '@mui/icons-material/Add';
    import Tabs from '@mui/material/Tabs';
    //                              ^^^^ 2nd level
    
    // ❌ NOT OK
    import TabIndicator from '@mui/material/Tabs/TabIndicator';
    //                                           ^^^^^^^^^^^^ 3rd level 

    (Source: https://mui.com/material-ui/guides/minimizing-bundle-size/#option-one-use-path-imports)

Between the two options above, subpath exports would probably be preferable, since there is nothing to stop users from violating a convention (besides their own conscience, perhaps 😄), but subpath exports actually enforce their contract:

Now only the defined subpath in "exports" can be imported by a consumer:

import submodule from 'es-module-package/submodule.js';
// Loads ./node_modules/es-module-package/src/submodule.js COPY

While other subpaths will error:


import submodule from 'es-module-package/private-module.js';
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED

Since the changes proposed here apply to multiple packages and possibly considered breaking, I was also wondering if the team can shed some light on a rule/heuristic/convention, if any, which users of Lexical can rely on to know whether an import path is public or private? It would really help to prevent inadvertently building a dependency on something package-private.

Thank you in advance!

acywatson commented 11 months ago

Thanks for your thoughts here! I like the subpath idea. I have seen this before, but wasn't aware that there was a convention around it. Seems like something we could consider adopting.

To help out out practically here - it has been our practice to consider anything exported from a public package to be part of our public API for purposes of versioning/support. In other words, we stick to the rules of semantic versioning for any of that.

I still think it would be valuable to communciate it implicitly in one of the ways you describe, but hopefully this helps you feel comfortable moving forward for now.

mxxk commented 11 months ago

Thanks for your thoughts here! I like the subpath idea. I have seen this before, but wasn't aware that there was a convention around it. Seems like something we could consider adopting.

To help out out practically here - it has been our practice to consider anything exported from a public package to be part of our public API for purposes of versioning/support. In other words, we stick to the rules of semantic versioning for any of that.

I still think it would be valuable to communicate it implicitly in one of the ways you describe, but hopefully this helps you feel comfortable moving forward for now.

Thank you for your clarification @acywatson! If I understood correctly, Lexical has some packages which are considered "public", and other packages which are considered "private" (and public packages call private packages as needed). Nifty!

As far as figuring out which packages are public, would that be the list explicitly documented on the website?

https://github.com/facebook/lexical/blob/2c825792956f219f97da88c455d53855ddb1d9f5/packages/lexical-website/sidebars.js#L54-L77

If this is so, then other packages (e.g., @lexical/overflow) are private. Is this accurate?

acywatson commented 11 months ago

We enforce private methods via module exports. No package is entirely private. If you can import a function from that package through conventional means (i.e., if it's exported from the publicly vended package artifacts on NPM), then we treat it as part of our public API.

An example of something that's NOT part of the public API would be ImageNode - anything in the playground, basically.

mxxk commented 11 months ago

Understood, thanks again. My original question around public/private package API has been answered, so this issue can be closed, or if it helps to keep it open in case you wanted to pursue subpath exports, however would be best!

etrepum commented 6 months ago

I think all of the issues around this have been resolved in the process of supporting ESM. The public APIs should all show up in the API documentation as well since we have consolidated all of the logic for what's public and what's private and the build and docs related tools all use it now.

It's not using subpath exports because of some implementation details in the way we did it, all of the modules that are exported from each package are explicit.

mxxk commented 6 months ago

Thanks @etrepum! Closing as completed.