Skip to content
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

Merged
merged 46 commits into from
Jul 20, 2020

Conversation

benjamn
Copy link
Member

@benjamn benjamn commented Jul 20, 2020

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 both main and module fields, along with an index.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's index.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.

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.
Comment on lines +30 to +42
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);
Copy link
Member Author

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).

Comment on lines -9 to +14
import { addTypenameToDocument } from '../../utilities/graphql/transform';
import { StoreObject, Reference, isReference } from '../../utilities/graphql/storeUtils';
import {
addTypenameToDocument,
StoreObject,
Reference,
isReference,
} from '../../utilities';
Copy link
Member Author

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';
Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

@hwillson hwillson left a 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
Copy link
Member

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.

Comment on lines +1 to +26
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"] },
];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this approach! 🎉

Comment on lines +65 to +71
it("completeness", () => {
const { join } = require("path").posix;
entryPoints.forEach((info: Record<string, any>) => {
const id = join("@apollo/client", ...info.dirs);
expect(testedIds).toContain(id);
});
});
Copy link
Member

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! 😂

@benjamn benjamn merged commit 7993a77 into master Jul 20, 2020
benjamn added a commit that referenced this pull request Jul 20, 2020
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.
benjamn added a commit that referenced this pull request Jul 20, 2020
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.
benjamn added a commit that referenced this pull request Jul 20, 2020
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.
benjamn added a commit that referenced this pull request Jul 20, 2020
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.
benjamn added a commit that referenced this pull request Jul 21, 2020
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.
benjamn added a commit that referenced this pull request Jul 21, 2020
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.
benjamn added a commit that referenced this pull request Jul 21, 2020
@benjamn benjamn deleted the revamp-commonjs-interdependencies branch July 21, 2020 14:48
jimrandomh pushed a commit to jimrandomh/apollo-client that referenced this pull request Jul 22, 2020
…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.
jimrandomh pushed a commit to jimrandomh/apollo-client that referenced this pull request Jul 22, 2020
benjamn added a commit that referenced this pull request Jul 28, 2020
These types were lost in #6656 when I switched from

  export * from '../cache'

to using named exports. There are some @apollo/client/cache exports that I
wanted to omit from @apollo/client/core (like cacheSlot), but I definitely
did not intend to omit these types.

Fixes #6723.
benjamn added a commit that referenced this pull request Jul 28, 2020
These types were lost in #6656 when I switched from

  export * from '../cache'

to using named exports. There are some @apollo/client/cache exports that I
wanted to omit from @apollo/client/core (like cacheSlot), but I definitely
did not intend to omit these types.

Fixes #6723.
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants