Skip to content

Jayden's optimal JavaScript module design topic #161

@benjie

Description

@benjie

Hi folks, sorry I had to drop unannounced yesterday.

Having had to deal with the issue that @jaydenseric raises on many occasions, I fully sympathise with the motivation. Lodash, MUI, and similar frameworks have the pattern of import thing from 'module/thing' and have codemods that can update your codebase to use them, but if you have a single dependency somewhere that does import { thing } from 'module' then all of that effort is pointless. I've had to use patch-package and similar to deeply edit node_modules to change dependencies to importing things from the right place, and it can get old fast!

The very simplest way forward, to try this out, would be to add explicit entrypoints in a flat structure for all our existing exports. The advantage of a flat structure is that a) we don't need to figure out what tree structure we want, b) we don't need to do a semver major release when we refactor internals changing this structure, and c) it's incredibly easy to write a codemod: import { foo as bar } from 'graphql' simply gets rewritten to import bar from 'graphql/foo' and you're done.

I would then suggest that the entire rest of GraphQL.js would be moved into a folder (still in a tree structure) called __private__ or similar, and the 200 or so public entrypoints can import from the __private__/** paths as needed. This would make it very explicit if a user was importing what we consider a "private" utility, and would make it obvious to them that it might break in a patch release.

I echo @enisdenjo's concerns about splitting things up too far without solid use-cases; for example I cannot understand why someone would want GraphQLInt on its own without also wanting e.g. validation or schema or execution; and with any of those things you're going to need GraphQLString and GraphQLBoolean (even if just for the introspection schema). The same goes for the builtin directives. A dogmatic approach to this yields the benefit of simplicity and ease of adoption via codemods, but there is the risk that we're not reaping the full rewards if we introduce more overhead by splitting things up too much - e.g. each module may require fetching (which has HTTP overhead, even when fetched in parallel and multiplexed over HTTP/2+) or may be wrapped in its own little closure (I think this is how webpack works still?) which introduces both size and memory usage overheads. That said, I'm happy to assume that the benefits are worth the cost and I'm in favour of the simplistic dogmatic approach that's easy to codemod.

@jaydenseric perhaps you can lay out some concrete use cases where an application might import just one of the builtin scalars and not use any other part of graphql-js? That would make this approach more clearly the right direction.

On the "no index file" front, I think we should reserve that for the next server major. In the release we introduce the subpaths, I'd re-export everything from the root barrel file but mark it all as deprecated (e.g. via tsdoc) to encourage users to move to the new way. Sometimes it takes a while to fully update a codebase to use the new patterns, so giving users a period when both the root and the subpaths work is going to give them a window to adopt this practice gracefully before we force it upon them in the next semver major. It also means that we can give users an entire release cycle (e.g. v18 -> v19) to prepare for the breaking change coming down the line.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions