Skip to content

fix(build): Ensure bundle builds have needed dependencies #5119

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

Merged
merged 2 commits into from
May 18, 2022

Conversation

lobsterkatie
Copy link
Member

@lobsterkatie lobsterkatie commented May 17, 2022

In #5094, a change was made to parallelize our repo-level build commands as much as possible. In that PR, it was stated that build:bundle could run independent of, and therefore in parallel with, the types and rollup builds, and at the time that was true. When TS (through rollup) builds a bundle, it creates a giant AST out of the bundled package and all of its monorepo dependencies, based on the original source code, then transpiles the whole thing - no prework needed.

But in #5111 we switched our ES6 bundles to use sucrase for transpilation (still through rollup), and that changed things. Sucrase (along with every other non-TS transpilation tool) only considers files one by one, not as part of a larger whole, and won't reach across package boundaries, even within the monorepo. As a result, rollup needs all dependencies to already exist in transpiled form, since sucrase doesn't touch them, which becomes a problem if both processes are happening at once.

(But how has CI even been passing since that second PR, then?, you may ask. The answer is, sucrase is very fast, and lerna can only start so many things at once. It ends up being a race condition between sucrase finishing with the dependencies and lerna kicking off the bundle builds, and almost all the time, sucrase wins. And the other situations in which this is broken all involve using something other than the top-level build script to create bundles in a repo with no existing build artifacts, and CI just never does that.)

So TL;DR, we need to have already transpiled a packages's monorepo dependencies before that package can be turned into a bundle. For build:bundle at both the repo and package level, and for build at the package level, this means that if they're not there, we have to build them. For build at the repo level, where transpilation of all packages does in fact already happen, we have two options:

  1. Push bundle builds to happen alongside build:extras, after rollup builds have finished.
  2. Mimic what happens when the race condition is successful, but in a way that isn't flaky. In other words, continue to run build:bundle in parallel with the types and npm package builds, but guarantee that the needed dependencies have finished building themselves before starting the bundle build.

Of the two options, the first is certainly simpler, but it also forces the two longest parts of the build (bundle and types builds) to be sequential, which is the exact opposite of what we want, given that the goal all along has been to make the builds noticeably faster. Choosing the second solution also gives us an excuse to add the extra few lines of code needed to fix the repo-level-build:bundle/package-level-build/package-level-build:bundle problem, which we otherwise probably wouldn't fix.

This implements that second strategy, by polling at 5 second intervals for the existence of the needed files, for up to 60 seconds, before beginning the bundle builds. (In practice, it fairly reliably seems to take two retries, or ten seconds, before the bundle build can begin.) It also handles the other three situations above, by building the missing files when necessary. Finally, it fixes a type import to rely on the main types package, rather than a built version of it. This prevents our ES5 bundles (which still use TS for transpilation, in order to also take advantage of its ability to down-compile) from running to the same problem as the sucrase bundles are having.

@github-actions
Copy link
Contributor

github-actions bot commented May 17, 2022

size-limit report 📦

Path Size
@sentry/browser - ES5 CDN Bundle (gzipped + minified) 18.75 KB (-6.91% 🔽)
@sentry/browser - ES5 CDN Bundle (minified) 58.19 KB (-9.95% 🔽)
@sentry/browser - ES6 CDN Bundle (gzipped + minified) 17.65 KB (-6.44% 🔽)
@sentry/browser - ES6 CDN Bundle (minified) 52.09 KB (-10.14% 🔽)
@sentry/browser - Webpack (gzipped + minified) 19.33 KB (-16.84% 🔽)
@sentry/browser - Webpack (minified) 61.44 KB (-24.82% 🔽)
@sentry/react - Webpack (gzipped + minified) 19.35 KB (-16.88% 🔽)
@sentry/nextjs Client - Webpack (gzipped + minified) 42.81 KB (-10.92% 🔽)
@sentry/browser + @sentry/tracing - ES5 CDN Bundle (gzipped + minified) 24.41 KB (-6.4% 🔽)
@sentry/browser + @sentry/tracing - ES6 CDN Bundle (gzipped + minified) 23 KB (-6.08% 🔽)

// Checking that the directory exists isn't 100% the same as checking that the files themselves exist, of course,
// but it's a decent proxy, and much simpler to do than checking for individual files. (We're arbitrarily checking
// for the `cjs` directory here rather than the `esm` directory; they're built together so either one will do.)
if (!fs.existsSync(`${depBuildDir}/cjs`) && !fs.existsSync(`${depBuildDir}/npm/cjs`)) {
Copy link
Member

Choose a reason for hiding this comment

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

we could just as easily check for both esm and cjs - might be worthwhile just to have anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, you're reminding me that it's actually the esm that it uses, because we use the default for this @rollup/plugin-node-resolve option. Added it to the check.

}): Promise<void> {
const { maxRetries = 12, dependencies } = options;

const processDotEnv = process.env as ProcessDotEnv;
Copy link
Member

Choose a reason for hiding this comment

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

I mean I would rather just treat these as possible to be undefined and do existence checks on INIT_CWD and npm_config_argv instead of using a custom ProcessDotEnv type. Let's just play on the safe side here.

const initCwd = process.env.INIT_CWD;
if (initCwd === undefined) {
  console.error("INIT_CWD environment variable not defined");
  return;
}

const npmConfigArgv = process.env.npm_config_argv;
if (npmConfigArgv === undefined) {
  console.error("npm_config_argv environment variable not defined");
  return;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

IDK, I feel like the only way this breaks is if we upgrade yarn (at which point we'd know to be watching for breakage), and the worst that happens if it does is our build won't go. Feels fairly low stakes to me. But, anyway, added a check.


console.log('\nSearching for bundle dependencies...');

while (retries < maxRetries && !checkForBundleDeps(packagesDir, dependencyDirs)) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: This should really be a helper func which just retries a generic func a certain num of times.

Copy link
Member Author

@lobsterkatie lobsterkatie May 17, 2022

Choose a reason for hiding this comment

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

Meh? I feel like there's enough bespoke stuff here that refactoring it into a generic retry-er would take some effort, and AFAIK we don't have a general need for such a thing at the moment. I'd rather put off that work until it's actually needed. (Which, to be fair, will happen if we try to apply this strategy to other parts of the build. But let's handle that then, so we can get this fix in.)

/**
* Wait the given number of milliseconds before continuing.
*/
async function sleep(ms: number): Promise<void> {
Copy link
Member

Choose a reason for hiding this comment

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

looking forward to us having this dev-utils package!

Copy link
Member Author

Choose a reason for hiding this comment

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

Same! That run function is in practically every script I write, LOL.

@lobsterkatie lobsterkatie force-pushed the kmclb-fix-repo-level-bundle-build-race-condition branch 3 times, most recently from a464a86 to 74c45ff Compare May 17, 2022 22:52
@lobsterkatie lobsterkatie force-pushed the kmclb-fix-repo-level-bundle-build-race-condition branch from 74c45ff to 22bdad2 Compare May 18, 2022 02:07
@lobsterkatie lobsterkatie merged commit a917dda into 7.x May 18, 2022
@lobsterkatie lobsterkatie deleted the kmclb-fix-repo-level-bundle-build-race-condition branch May 18, 2022 03:02
AbhiPrasad pushed a commit that referenced this pull request May 30, 2022
In #5094, a change was made to parallelize our repo-level build commands as much as possible. In that PR, it was stated that `build:bundle` could run independent of, and therefore in parallel with, the types and rollup builds, and at the time that was true. When TS (through rollup) builds a bundle, it creates a giant AST out of the bundled package and all of its monorepo dependencies, based on the original source code, then transpiles the whole thing - no prework needed.

But in #5111 we switched our ES6 bundles to use sucrase for transpilation (still through rollup), and that changed things. Sucrase (along with every other non-TS transpilation tool) only considers files one by one, not as part of a larger whole, and won't reach across package boundaries, even within the monorepo. As a result, rollup needs all dependencies to already exist in transpiled form, since sucrase doesn't touch them, which becomes a problem if both processes are happening at once.

(_But how has CI even been passing since that second PR, then?_, you may ask. The answer is, sucrase is very fast, and lerna can only start so many things at once. It ends up being a race condition between sucrase finishing with the dependencies and lerna kicking off the bundle builds, and almost all the time, sucrase wins. And the other situations in which this is broken all involve using something other than the top-level `build` script to create bundles in a repo with no existing build artifacts, and CI just never does that.)

So TL;DR, we need to have already transpiled a packages's monorepo dependencies before that package can be turned into a bundle. For `build:bundle` at both the repo and package level, and for `build` at the package level, this means that if they're not there, we have to build them. For `build` at the repo level, where transpilation of all packages does in fact already happen, we have two options:
1) Push bundle builds to happen alongside `build:extras`, after rollup builds have finished.
2) Mimic what happens when the race condition is successful, but in a way that isn't flaky. In other words, continue to run `build:bundle` in parallel with the types and npm package builds, but guarantee that the needed dependencies have finished building themselves before starting the bundle build.

Of the two options, the first is certainly simpler, but it also forces the two longest parts of the build (bundle and types builds) to be sequential, which is the exact _opposite_ of what we want, given that the goal all along has been to make the builds noticeably faster. Choosing the second solution also gives us an excuse to add the extra few lines of code needed to fix the repo-level-build:bundle/package-level-build/package-level-build:bundle problem, which we otherwise probably wouldn't fix. 

This implements that second strategy, by polling at 5 second intervals for the existence of the needed files, for up to 60 seconds, before beginning the bundle builds. (In practice, it fairly reliably seems to take two retries, or ten seconds, before the bundle build can begin.) It also handles the other three situations above, by building the missing files when necessary. Finally, it fixes a type import to rely on the main types package, rather than a built version of it. This prevents our ES5 bundles (which still use TS for transpilation, in order to also take advantage of its ability to down-compile) from running to the same problem as the sucrase bundles are having.
@AbhiPrasad AbhiPrasad added this to the 7.0.0 milestone May 30, 2022
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