-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework interdependencies between @apollo/client/* entry points. #6656
Conversation
Before we get started refactoring how entry points are built, let's take a snapshot of the current exports of all public entry points.
This new module provides a single source of truth for all the nested entry points that we build. As a nice bonus, thanks to this unification, the @apollo/client/testing entry point no longer needs such special treatment.
This prevents collision between the actual main entry point (used by Node.js) and the dist/apollo-client.cjs.js bundle that we use for estimating the minified bundle size.
Adapted from https://github.com/apollographql/invariant-packages/tree/master/packages/rollup-plugin-invariant, this script wraps invariant(...), new InvariantError(...), and invariant.{warn,error} calls with process.env.NODE_ENV logic, so that long error messages can be stripped from production builds. I decided to abandon the Rollup plugin for this task because I'm planning to change how Rollup determines if imported modules are "external," and Rollup (understandably) stops traversing the dependency graph when it hits an external import, which means we can no longer rely on Rollup to traverse all modules that need invariants to be transformed. Also, this transformation step was never especially intertwined with the dependency traversal and rewriting logic that Rollup provides, so I think it makes things simpler to keep them separate. If Rollup provided hooks for performing arbitrary build steps, we might have been able to do this work in some initial step, but I couldn't find anything in the Rollup documentation that fit the bill (and that's fine: I wouldn't expect Rollup to attempt to be a generic build pipeline). Now, the CJS bundles for each nested entry point (e.g. @apollo/client/core or @apollo/client/react/ssr) can be computed from code that has been processed by the invariant transform, but has not yet been tampered with by Rollup, so we can trust that the exact same module identifiers from the source code are still intact.
There's not currently any risk of running the invariant transform over the ./dist directory more than once during our build process, but it doesn't hurt to make sure the transform is idempotent.
Since we aren't using Rollup to apply the invariant transform anymore, it turns out we don't need this build step at all. Instead, the CJS bundles will be generated directly from the modules generated by tsc and processed by the invariant transform.
This is a vital change (the whole point of this PR, really), because it means Rollup will no longer attempt to include code from other entry points in the CJS bundles for unrelated entry points, so there will no longer be any duplication of code between the CJS bundles.
It would be nice to make this an error that would fail the build, but there are a couple of benign violations that would be disruptive to fix at this point, like reexporting `../ApolloClient` from `./core/QueryManager`, and `./testing/index.ts` reaching into `../utilities/testing`.
Now that the CommonJS module tree uses nested entry points with their own package.json files, instead of importing everything and then filtering it (as the CJS version of @apollo/client/core used to do), we do not need to be so paranoid about the possibility that React is not installed.
describe('exports of public entry points', () => { | ||
const testedIds = new Set<string>(); | ||
|
||
function check(id: string, ns: Namespace) { | ||
it(id, () => { | ||
testedIds.add(id); | ||
expect(Object.keys(ns).sort()).toMatchSnapshot(); | ||
}); | ||
} | ||
|
||
check("@apollo/client", client); | ||
check("@apollo/client/cache", cache); | ||
check("@apollo/client/core", core); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be ideal to check the CJS exports using require
(as well as ESM exports, which this test is doing), but Jest doesn't run the build first, and it compiles code directly from ./src
rather than looking inside ./dist
(which is where all the CJS code ends up).
import { addTypenameToDocument } from '../../utilities/graphql/transform'; | ||
import { StoreObject, Reference, isReference } from '../../utilities/graphql/storeUtils'; | ||
import { | ||
addTypenameToDocument, | ||
StoreObject, | ||
Reference, | ||
isReference, | ||
} from '../../utilities'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Importing from the entry point directory (rather than reaching inside of it) has the nice side-benefit of allowing multiple import
statements to be merged into one, in many cases.
@@ -0,0 +1 @@ | |||
export * from '../utilities/testing'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would love to ditch this module and just move src/utilities/testing
to src/testing
, but that would have required a lot of changes to imports across our test suite, so it seemed potentially disruptive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an example of one of the warnings from f634785 that this code causes:
dist/testing/index.js → dist/testing/testing.cjs.js...
Risky cross-entry-point nested import of ../utilities/testing in testing/index.js
created dist/testing/testing.cjs.js in 24ms
The problem is that ../utilities
is an entry point directory, so importing something from inside it is invasive. In this particular case, it's mostly fine because the @apollo/client/utilities
doesn't use or export anything from the utilities/testing
directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incredible @benjamn, and really helped cleanup the build process / imports as a nice side effect. Thanks very much for working through all of this!
@@ -1,3 +1,10 @@ | |||
## Apollo Client 3.1.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm 👍 on targeting 3.1.0 for this.
const entryPoints = [ | ||
{ dirs: [], bundleName: "main" }, | ||
{ dirs: ['cache'] }, | ||
{ dirs: ['core'] }, | ||
{ dirs: ['errors'] }, | ||
{ dirs: ['link', 'batch'] }, | ||
{ dirs: ['link', 'batch-http'] }, | ||
{ dirs: ['link', 'context'] }, | ||
{ dirs: ['link', 'core'] }, | ||
{ dirs: ['link', 'error'] }, | ||
{ dirs: ['link', 'http'] }, | ||
{ dirs: ['link', 'retry'] }, | ||
{ dirs: ['link', 'schema'] }, | ||
{ dirs: ['link', 'utils'] }, | ||
{ dirs: ['link', 'ws'] }, | ||
{ dirs: ['react'] }, | ||
{ dirs: ['react', 'components'] }, | ||
{ dirs: ['react', 'context'] }, | ||
{ dirs: ['react', 'data'] }, | ||
{ dirs: ['react', 'hoc'] }, | ||
{ dirs: ['react', 'hooks'] }, | ||
{ dirs: ['react', 'parser'] }, | ||
{ dirs: ['react', 'ssr'] }, | ||
{ dirs: ['utilities'] }, | ||
{ dirs: ['testing'], extensions: [".js", ".jsx"] }, | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this approach! 🎉
it("completeness", () => { | ||
const { join } = require("path").posix; | ||
entryPoints.forEach((info: Record<string, any>) => { | ||
const id = join("@apollo/client", ...info.dirs); | ||
expect(testedIds).toContain(id); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding this - I would have been the first one to forget! 😂
This partially reverts commit 627bd1b. PR #6656 removed a function called `prepareESM`, which was no longer needed for the sake of running `rollup-plugin-invariant`, but which had another benefit: Rollup would replace imports/exports like // Example exports from ./dist/index.js: export * from './core' export * from './react' with their fully-expanded (and file-extensioned) equivalents: ... export { makeVar } from './cache/inmemory/reactiveVars.js'; export { defaultDataIdFromObject } from './cache/inmemory/policies.js'; export { InMemoryCache } from './cache/inmemory/inMemoryCache.js'; export { ApolloClient } from './core/ApolloClient.js'; ... Although this may look like more code, it's effectively what the bundler/browser will have to do anyway, and including the `.js` file extensions makes this code a little more browser- and Rollup-friendly (that is, you shouldn't have to use @rollup/plugin-node-resolve to bundle @apollo/client with Rollup). The expanded imports/exports are also closer to the ESM code shipped with @apollo/client@3.0.x, so restoring this behavior helps with the goal of keeping the changes in #6656 backwards-compatible for v3.1.0. Note that the CommonJS bundles used by Node.js are built before this step, so this expansion of imports does not affect Node.js.
This partially reverts commit 627bd1b. PR #6656 removed a function called `prepareESM`, which was no longer needed for the sake of running `rollup-plugin-invariant`, but which had another benefit: Rollup would replace imports/exports like // Example exports from ./dist/index.js: export * from './core' export * from './react' with their fully-expanded (and file-extensioned) equivalents: ... export { makeVar } from './cache/inmemory/reactiveVars.js'; export { defaultDataIdFromObject } from './cache/inmemory/policies.js'; export { InMemoryCache } from './cache/inmemory/inMemoryCache.js'; export { ApolloClient } from './core/ApolloClient.js'; ... Although this may look like more code, it's effectively what the bundler/browser will have to do anyway, and including the `.js` file extensions makes this code a little more browser- and Rollup-friendly (that is, you shouldn't have to use @rollup/plugin-node-resolve to bundle @apollo/client with Rollup). The expanded imports/exports are also closer to the ESM code shipped with @apollo/client@3.0.x, so restoring this behavior helps with the goal of keeping the changes in #6656 backwards-compatible for v3.1.0. Note that the CommonJS bundles used by Node.js are built before this step, so this expansion of imports does not affect Node.js.
This partially reverts commit 627bd1b. PR #6656 removed a function called `prepareESM`, which was no longer needed for the sake of running `rollup-plugin-invariant`, but which had another benefit: Rollup would replace imports/exports like // Example exports from ./dist/index.js: export * from './core' export * from './react' with their fully-expanded (and file-extensioned) equivalents: ... export { makeVar } from './cache/inmemory/reactiveVars.js'; export { defaultDataIdFromObject } from './cache/inmemory/policies.js'; export { InMemoryCache } from './cache/inmemory/inMemoryCache.js'; export { ApolloClient } from './core/ApolloClient.js'; ... Although this may look like more code, it's effectively what the bundler/browser will have to do anyway, and including the `.js` file extensions makes this code a little more browser- and Rollup-friendly (that is, you shouldn't have to use @rollup/plugin-node-resolve to bundle @apollo/client with Rollup). The expanded imports/exports are also closer to the ESM code shipped with @apollo/client@3.0.x, so restoring this behavior helps with the goal of keeping the changes in #6656 backwards-compatible for v3.1.0. Note that the CommonJS bundles used by Node.js are built before this step, so this expansion of imports does not affect Node.js.
This partially reverts commit 627bd1b. PR #6656 removed a function called `prepareESM`, which was no longer needed for the sake of running `rollup-plugin-invariant`, but which had another benefit: Rollup would replace imports/exports like // Example exports from ./dist/index.js: export * from './core' export * from './react' with their fully-expanded (and file-extensioned) equivalents: ... export { makeVar } from './cache/inmemory/reactiveVars.js'; export { defaultDataIdFromObject } from './cache/inmemory/policies.js'; export { InMemoryCache } from './cache/inmemory/inMemoryCache.js'; export { ApolloClient } from './core/ApolloClient.js'; ... Although this may look like more code, it's effectively what the bundler/browser will have to do anyway, and including the `.js` file extensions makes this code a little more browser- and Rollup-friendly (that is, you shouldn't have to use @rollup/plugin-node-resolve to bundle @apollo/client with Rollup). The expanded imports/exports are also closer to the ESM code shipped with @apollo/client@3.0.x, so restoring this behavior helps with the goal of keeping the changes in #6656 backwards-compatible for v3.1.0. Note that the CommonJS bundles used by Node.js are built before this step, so this expansion of imports does not affect Node.js.
This partially reverts commit 627bd1b. PR #6656 removed a function called `prepareESM`, which was no longer needed for the sake of running `rollup-plugin-invariant`, but which had another benefit: Rollup would replace imports/exports like // Example exports from ./dist/index.js: export * from './core' export * from './react' with their fully-expanded (and file-extensioned) equivalents: ... export { makeVar } from './cache/inmemory/reactiveVars.js'; export { defaultDataIdFromObject } from './cache/inmemory/policies.js'; export { InMemoryCache } from './cache/inmemory/inMemoryCache.js'; export { ApolloClient } from './core/ApolloClient.js'; ... Although this may look like more code, it's effectively what the bundler/browser will have to do anyway, and including the `.js` file extensions makes this code a little more browser- and Rollup-friendly (that is, you shouldn't have to use @rollup/plugin-node-resolve to bundle @apollo/client with Rollup). The expanded imports/exports are also closer to the ESM code shipped with @apollo/client@3.0.x, so restoring this behavior helps with the goal of keeping the changes in #6656 backwards-compatible for v3.1.0. Note that the CommonJS bundles used by Node.js are built before this step, so this expansion of imports does not affect Node.js.
This partially reverts commit 627bd1b from PR #6656. PR #6656 removed a function called `prepareESM`, which was no longer needed for the sake of running `rollup-plugin-invariant`, but which had another benefit: Rollup would replace imports/exports like // Example exports from ./dist/index.js: export * from './core' export * from './react' with their fully-expanded (and file-extensioned) equivalents: ... export { makeVar } from './cache/inmemory/reactiveVars.js'; export { defaultDataIdFromObject } from './cache/inmemory/policies.js'; export { InMemoryCache } from './cache/inmemory/inMemoryCache.js'; export { ApolloClient } from './core/ApolloClient.js'; ... Although this may look like more code, it effectively saves the bundler/browser work by reducing indirection, and including the .js file extensions makes this code a little more browser- and Rollup-friendly (that is, you shouldn't have to use @rollup/plugin-node-resolve to bundle @apollo/client with Rollup). The expanded imports/exports are also closer to the ESM code shipped with @apollo/client@3.0.x, so restoring this behavior helps with the goal of keeping the changes in #6656 backwards-compatible for v3.1.0. Note that the CommonJS bundles used by Node.js are built before this step, so this expansion of imports does not affect Node.js.
…6657) This partially reverts commit 627bd1b from PR apollographql#6656. PR apollographql#6656 removed a function called `prepareESM`, which was no longer needed for the sake of running `rollup-plugin-invariant`, but which had another benefit: Rollup would replace imports/exports like // Example exports from ./dist/index.js: export * from './core' export * from './react' with their fully-expanded (and file-extensioned) equivalents: ... export { makeVar } from './cache/inmemory/reactiveVars.js'; export { defaultDataIdFromObject } from './cache/inmemory/policies.js'; export { InMemoryCache } from './cache/inmemory/inMemoryCache.js'; export { ApolloClient } from './core/ApolloClient.js'; ... Although this may look like more code, it effectively saves the bundler/browser work by reducing indirection, and including the .js file extensions makes this code a little more browser- and Rollup-friendly (that is, you shouldn't have to use @rollup/plugin-node-resolve to bundle @apollo/client with Rollup). The expanded imports/exports are also closer to the ESM code shipped with @apollo/client@3.0.x, so restoring this behavior helps with the goal of keeping the changes in apollographql#6656 backwards-compatible for v3.1.0. Note that the CommonJS bundles used by Node.js are built before this step, so this expansion of imports does not affect Node.js.
Now that we've added a bunch of new entry points for importing specific parts of Apollo Client (e.g.
@apollo/client/react/*
in #5357 and #6558 and@apollo/client/link/*
in #6577), it's important to consider how they all relate to each other.If we let Rollup do its thing, any shared code imported by multiple different entry point bundles will end up duplicated across the bundles, which is always a bundle size concern, and often a threat to correctness, especially in cases where important classes and functions are assumed to be singletons.
Note: these problems primarily affect the CommonJS bundles (
dist/**/*.cjs.js
) built by Rollup from TypeScript-compiled ESM modules (and consumed primarily by Node.js), and are less of a threat for the ESM modules, since the structure of the dependency graph is preserved by the ESM modules, allowing most bundlers to avoid duplication of modules when bundling ESM code.To fix these problems once and for all, we need to teach Rollup that importing from a different entry point counts as an external import, so Rollup will not bundle the imported code into the current bundle, or try to rewrite the imported module identifier in any way.
Then, we need to avoid any nested imports that reach inside other entry points, and instead enforce that any cross-entry-point imports import only from the entry point directory, which must have a
package.json
with bothmain
andmodule
fields, along with anindex.ts
file that defines the exports of the entry point. These kinds of changes account for most of the lines modified by this PR. If an entry point doesn't export something, it's not okay to reach inside the entry point and pull out the thing you want, because that leads to duplication. Instead, the entry point'sindex.ts
file will need to explicitly export anything needed by code outside of the entry point.Finally, to minimize the risk that this PR will introduce breaking changes, we can use snapshot testing to verify that the set of exports for each entry point has not changed in unexpected ways. I'm very happy with how this turned out (see the very first commit, and the final few commits, specifically).
There's a lot here, but I recommend reviewing commit-by-commit. You can breeze through the
Fix cross-entry-point imports in ...
commits once you get the hang of them, but the others are pretty important.