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

ref(nextjs): Use proxy loader for wrapping all data-fetching functions #5602

Merged
merged 5 commits into from
Aug 19, 2022

Conversation

lobsterkatie
Copy link
Member

@lobsterkatie lobsterkatie commented Aug 18, 2022

This changes the experimental auto-wrapping feature to use a templated proxy module for all wrapping. (Previously, the wrapping was done using a different mix of code parsing, AST manipulation, proxying, templating, and string concatenation for each function.) Not only should this be easier to reason about and maintain (because it's one unified method of wrapping), it also solves a number of the disadvantages inherent in various parts of the previous approach. Specifically, using a template for everything rather than storing code as strings lets us take advantage of linting and syntax highlighting, and using a proxy loader rather than dealing with the AST directly puts the onus of handling syntax variations and edge cases on tools which are actually designed for that purpose.

At a high level, the proxy loader works like this:

  • During the nextjs build phase, webpack loads all of the files in the pages directory, and feeds each one to our loader.
  • The loader derives the parameterized route from the page file's path, and fills it and the path itself into the template.
  • In the template, we load the page module, replace any data-fetching methods we find with wrapped versions of themselves, and then re-export everything.
  • The contents of the template is returned by the loader in place of the original contents of the page module.

Previously, when working directly with the page module's AST, we had to account for the very many ways functions can be defined and exported. By contrast, doing the function wrapping in a separate module allows us to take advantage of the fact that imported modules have a single, known structure, which we can modify directly in the template code.

Notes:

  • For some reason, nextjs won't accept data fetchers which are exported as part of an export * from '...' statement. Therefore, the "re-export everything" part of the third step above needs to be of the form export { all, of, the, things, which, the, page, module, exports, listed, individually } from './pageModule'. This in turn requires knowing the full list of each page module's exports, since, unfortunately, export { ...importedPageModule } isn't a thing. As it turns out, one of the noticeable differences between our published code before and after the build process revamp in the spring is that where tsc leaves export * statements untouched, Rollup splits them out into individual exports - exactly what's needed here! The loader therefore uses Rollup's JS API to process the proxy module code before returning it. Further, in order that Rollup be able to understand the page module code (which will be written in either jsx or tsx), we first use Sucrase to transpile the code to vanilla JS. Who knew the build process work would come in so handy?

  • Given that we replace a page module's contents with the proxy code the first time webpack tries to load it, we need webpack to load the same module a second time, in order to be able to process and bundle the page module itself. We therefore attach a query string to the end of the page module's path wherever it's referenced in the template, because this makes Webpack think it is a different, as-yet-unloaded module, causing it to perform the second load. The query string also acts like a flag for us, so that the second time through we know we've already handled the file and can let it pass through the loader untouched.

  • Rollup expects the entry point to be given as a path to a file on disk, not as raw code. We therefore create a temporary file for each page's proxy module, which we then delete as soon as rollup has read it. The easiest way to make sure that relative paths are preserved when things are re-exported is to put each temporary file alongside the page module it's wrapping, in the pages/ directory. Fortunately, by the time our loader is running, Webpack has already decided what files it needs to load, so these temporary files don't get caught up in the bundling process.

  • In order to satisfy the linter in the template file, the SDK itself has been added as a dev dependency. Fortunately this seems not to confuse yarn.

  • Just to keep things manageable, this stops using but doesn't yet delete the previous loader (and associated files/code). Once this is merged, I'll do that in a separate PR.

Ref #5505

@lobsterkatie lobsterkatie mentioned this pull request Aug 18, 2022
43 tasks
Copy link
Member

@lforst lforst left a comment

Choose a reason for hiding this comment

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

Best. Change. Ever. I'm super impressed that you managed to make it very clear what's happening, given that all this is rather complicated. Glad to see this "theory" become reality.

See this PR more or less as approved. Comments are either questions or nits.

There is one high-level topic I wanna discuss before actually pressing the green checkmark button (see the comment about maybe bundling rollup in the loader).

packages/nextjs/src/config/loaders/proxyLoader.ts Outdated Show resolved Hide resolved
exports: 'named',
},
external: ['@rollup/plugin-sucrase', 'rollup'],
Copy link
Member

Choose a reason for hiding this comment

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

Just had a thought. Considering we depend on a very specific behaviour of rollup (i.e. the unwrapping of the * in export * from ...), what do you think about doing the following two things:

  • We pin the exact version of rollup we're using so this behaviour doesn't just randomly break with an update.
  • We bundle rollup within our loader so that pinning the version doesn't turn into a dependency fiasco for our downstream users.

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. Pinning I think is a good idea (which is why I did it! 😛)

  2. You mean like vendoring the code ourselves? What kind of fiasco are you picturing?

Copy link
Member

Choose a reason for hiding this comment

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

  1. Sorry I am dumb. Sometimes im just writing down thoughts as suggestions without checking if they arent already there. Good that we're on the same page there.
  2. I am double dumb. The dep problem i was thinking of is with peer deps and not normal deps. I think we're good here!

Copy link
Member Author

Choose a reason for hiding this comment

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

LOL. Terrific. Now we're even for the other day when I entirely forgot the wrappers existed for about five minutes. And I'm the one who originally wrote them!

packages/nextjs/src/config/webpack.ts Show resolved Hide resolved
packages/nextjs/test/run-integration-tests.sh Outdated Show resolved Hide resolved
packages/nextjs/src/config/loaders/rollup.ts Outdated Show resolved Hide resolved
packages/nextjs/src/config/loaders/rollup.ts Outdated Show resolved Hide resolved
Comment on lines 98 to 92
// Nextjs uses square brackets surrounding a path segment to denote a parameter in the route, but rollup turns those
// square brackets into underscores. Further, Rollup adds file extensions to bare-path-type import and export sources.
Copy link
Member

Choose a reason for hiding this comment

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

rollup turns those square brackets into underscores

wat

Jackie-Chan-WTF

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I know. I don't understand how that could possibly work, for anyone. I feel like it's gotta be a bug, which I should probably report...

packages/nextjs/src/config/loaders/rollup.ts Show resolved Hide resolved
@lobsterkatie lobsterkatie force-pushed the kmclb-nextjs-use-proxy-loader-for-all-wrapping branch from 6b358f7 to fe67cdb Compare August 19, 2022 03:54
@github-actions
Copy link
Contributor

size-limit report 📦

Path Size
@sentry/browser - ES5 CDN Bundle (gzipped + minified) 19.4 KB (-0.02% 🔽)
@sentry/browser - ES5 CDN Bundle (minified) 60.06 KB (0%)
@sentry/browser - ES6 CDN Bundle (gzipped + minified) 17.97 KB (-0.02% 🔽)
@sentry/browser - ES6 CDN Bundle (minified) 52.92 KB (0%)
@sentry/browser - Webpack (gzipped + minified) 19.77 KB (0%)
@sentry/browser - Webpack (minified) 64.31 KB (0%)
@sentry/react - Webpack (gzipped + minified) 19.79 KB (0%)
@sentry/nextjs Client - Webpack (gzipped + minified) 44.72 KB (0%)
@sentry/browser + @sentry/tracing - ES5 CDN Bundle (gzipped + minified) 25.91 KB (-0.01% 🔽)
@sentry/browser + @sentry/tracing - ES6 CDN Bundle (gzipped + minified) 24.28 KB (-0.01% 🔽)

@lobsterkatie
Copy link
Member Author

Best. Change. Ever.

Yeah, I'm actually pretty pleased with how clean it was in the end. It was a very good idea you had!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants