|
| 1 | +/* eslint-disable no-console */ |
| 2 | +import * as childProcess from 'child_process'; |
| 3 | +import * as fs from 'fs'; |
| 4 | +import * as path from 'path'; |
| 5 | +import * as util from 'util'; |
| 6 | + |
| 7 | +/** |
| 8 | + * Ensure that `build:bundle` has all of the dependencies it needs to run. Works at both the repo and package level. |
| 9 | + */ |
| 10 | +async function ensureBundleBuildPrereqs(options: { dependencies: string[]; maxRetries?: number }): Promise<void> { |
| 11 | + const { maxRetries = 12, dependencies } = options; |
| 12 | + |
| 13 | + const { |
| 14 | + // The directory in which the yarn command was originally invoked (which won't necessarily be the same as |
| 15 | + // `process.cwd()`) |
| 16 | + INIT_CWD: yarnInitialDir, |
| 17 | + // JSON containing the args passed to `yarn` |
| 18 | + npm_config_argv: yarnArgJSON, |
| 19 | + } = process.env; |
| 20 | + |
| 21 | + if (!yarnInitialDir || !yarnArgJSON) { |
| 22 | + const received = { INIT_CWD: yarnInitialDir, npm_config_argv: yarnArgJSON }; |
| 23 | + throw new Error( |
| 24 | + `Missing environment variables needed for ensuring bundle dependencies. Received:\n${util.inspect(received)}\n`, |
| 25 | + ); |
| 26 | + } |
| 27 | + |
| 28 | + // Did this build get invoked by a repo-level script, or a package-level script, and which script was it? |
| 29 | + const isTopLevelBuild = path.basename(yarnInitialDir) === 'sentry-javascript'; |
| 30 | + const yarnScript = (JSON.parse(yarnArgJSON) as { original: string[] }).original[0]; |
| 31 | + |
| 32 | + // convert '@sentry/xyz` to `xyz` |
| 33 | + const dependencyDirs = dependencies.map(npmPackageName => npmPackageName.split('/')[1]); |
| 34 | + |
| 35 | + // The second half of the conditional tests if this script is being run by the original top-level command or a |
| 36 | + // package-level command spawned by it. |
| 37 | + const packagesDir = isTopLevelBuild && yarnInitialDir === process.cwd() ? 'packages' : '..'; |
| 38 | + |
| 39 | + if (checkForBundleDeps(packagesDir, dependencyDirs)) { |
| 40 | + // We're good, nothing to do, the files we need are there |
| 41 | + return; |
| 42 | + } |
| 43 | + |
| 44 | + // If we get here, the at least some of the dependencies are missing, but how we handle that depends on how we got |
| 45 | + // here. There are six possibilities: |
| 46 | + // - We ran `build` or `build:bundle` at the repo level |
| 47 | + // - We ran `build` or `build:bundle` at the package level |
| 48 | + // - We ran `build` or `build:bundle` at the repo level and lerna then ran `build:bundle` at the package level. (We |
| 49 | + // shouldn't ever land here under this scenario - the top-level build should already have handled any missing |
| 50 | + // dependencies - but it's helpful to consider all the possibilities.) |
| 51 | + // |
| 52 | + // In the first version of the first scenario (repo-level `build` -> repo-level `build:bundle`), all we have to do is |
| 53 | + // wait, because other parts of `build` are creating them as this check is being done. (Waiting 5 or 10 or even 15 |
| 54 | + // seconds to start running `build:bundle` in parallel is better than pushing it to the second half of `build`, |
| 55 | + // because `build:bundle` is the slowest part of the build and therefore the one we most want to parallelize with |
| 56 | + // other slow parts, like `build:types`.) |
| 57 | + // |
| 58 | + // In all other scenarios, if the dependencies are missing, we have to build them ourselves - with `build:bundle` at |
| 59 | + // either level, we're the only thing happening (so no one's going to do it for us), and with package-level `build`, |
| 60 | + // types and npm assets are being built simultaneously, but only for the package being bundled, not for its |
| 61 | + // dependencies. Either way, it's on us to fix the problem. |
| 62 | + // |
| 63 | + // TODO: This actually *doesn't* work for package-level `build`, not because of a flaw in this logic, but because |
| 64 | + // `build:rollup` has similar dependency needs (it needs types rather than npm builds). We should do something like |
| 65 | + // this for that at some point. |
| 66 | + |
| 67 | + if (isTopLevelBuild && yarnScript === 'build') { |
| 68 | + let retries = 0; |
| 69 | + |
| 70 | + console.log('\nSearching for bundle dependencies...'); |
| 71 | + |
| 72 | + while (retries < maxRetries && !checkForBundleDeps(packagesDir, dependencyDirs)) { |
| 73 | + console.log('Bundle dependencies not found. Trying again in 5 seconds.'); |
| 74 | + retries += 1; |
| 75 | + await sleep(5000); |
| 76 | + } |
| 77 | + |
| 78 | + if (retries === maxRetries) { |
| 79 | + throw new Error( |
| 80 | + `\nERROR: \`yarn build:bundle\` (triggered by \`yarn build\`) cannot find its depdendencies, despite waiting ${ |
| 81 | + 5 * maxRetries |
| 82 | + } seconds for the rest of \`yarn build\` to create them. Something is wrong - it shouldn't take that long. Exiting.`, |
| 83 | + ); |
| 84 | + } |
| 85 | + |
| 86 | + console.log(`\nFound all bundle dependencies after ${retries} retries. Beginning bundle build...`); |
| 87 | + } |
| 88 | + |
| 89 | + // top-level `build:bundle`, package-level `build` and `build:bundle` |
| 90 | + else { |
| 91 | + console.warn('\nWARNING: Missing dependencies for bundle build. They will be built before continuing.'); |
| 92 | + |
| 93 | + for (const dependencyDir of dependencyDirs) { |
| 94 | + console.log(`\nBuilding \`${dependencyDir}\` package...`); |
| 95 | + run('yarn build:dev', { cwd: `${packagesDir}/${dependencyDir}` }); |
| 96 | + } |
| 97 | + |
| 98 | + console.log('\nAll dependencies built successfully. Beginning bundle build...'); |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +/** |
| 103 | + * See if all of the necessary dependencies exist |
| 104 | + */ |
| 105 | +function checkForBundleDeps(packagesDir: string, dependencyDirs: string[]): boolean { |
| 106 | + for (const dependencyDir of dependencyDirs) { |
| 107 | + const depBuildDir = `${packagesDir}/${dependencyDir}/build`; |
| 108 | + |
| 109 | + // Checking that the directory exists isn't 100% the same as checking that the files themselves exist, of course, |
| 110 | + // but it's a decent proxy, and much simpler to do than checking for individual files. (We're arbitrarily checking |
| 111 | + // for the `cjs` directory here rather than the `esm` directory; they're built together so either one will do.) |
| 112 | + if (!fs.existsSync(`${depBuildDir}/cjs`) && !fs.existsSync(`${depBuildDir}/npm/cjs`)) { |
| 113 | + // Fail fast |
| 114 | + return false; |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + return true; |
| 119 | +} |
| 120 | + |
| 121 | +/** |
| 122 | + * Wait the given number of milliseconds before continuing. |
| 123 | + */ |
| 124 | +async function sleep(ms: number): Promise<void> { |
| 125 | + await new Promise(resolve => |
| 126 | + setTimeout(() => { |
| 127 | + resolve(); |
| 128 | + }, ms), |
| 129 | + ); |
| 130 | +} |
| 131 | + |
| 132 | +/** |
| 133 | + * Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current |
| 134 | + * process. Returns contents of `stdout`. |
| 135 | + */ |
| 136 | +function run(cmd: string, options?: childProcess.ExecSyncOptions): string { |
| 137 | + return String(childProcess.execSync(cmd, { stdio: 'inherit', ...options })); |
| 138 | +} |
| 139 | + |
| 140 | +// TODO: Not ideal that we're hard-coding this, and it's easy to get when we're in a package directory, but would take |
| 141 | +// more work to get from the repo level. Fortunately this list is unlikely to change very often, and we're the only ones |
| 142 | +// we'll break if it gets out of date. |
| 143 | +const dependencies = ['@sentry/types', '@sentry/utils', '@sentry/hub', '@sentry/core']; |
| 144 | + |
| 145 | +if (['sentry-javascript', 'tracing', 'wasm'].includes(path.basename(process.cwd()))) { |
| 146 | + dependencies.push('@sentry/browser'); |
| 147 | +} |
| 148 | + |
| 149 | +void ensureBundleBuildPrereqs({ |
| 150 | + dependencies, |
| 151 | +}); |
0 commit comments