diff --git a/package.json b/package.json index e5df6c69b4a8..09848d739f99 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "scripts": { "build": "node ./scripts/verify-packages-versions.js && yarn run-p build:rollup build:types build:bundle && yarn build:extras", - "build:bundle": "lerna run --parallel build:bundle", + "build:bundle": "yarn ts-node scripts/ensure-bundle-deps.ts && yarn lerna run --parallel build:bundle", "build:dev": "run-p build:types build:rollup", "build:extras": "lerna run --parallel build:extras", "build:rollup": "lerna run --parallel build:rollup", diff --git a/packages/browser/package.json b/packages/browser/package.json index 05c3cd7b30e4..487294870c5b 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -44,7 +44,7 @@ }, "scripts": { "build": "run-p build:rollup build:bundle build:types", - "build:bundle": "rollup --config rollup.bundle.config.js", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", "build:dev": "run-p build:rollup build:types", "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", diff --git a/packages/hub/src/session.ts b/packages/hub/src/session.ts index fed5cfa004ad..c8cba3e30a04 100644 --- a/packages/hub/src/session.ts +++ b/packages/hub/src/session.ts @@ -1,5 +1,4 @@ -import { Session, SessionContext, SessionStatus } from '@sentry/types'; -import { SerializedSession } from '@sentry/types/build/types/session'; +import { SerializedSession, Session, SessionContext, SessionStatus } from '@sentry/types'; import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils'; /** diff --git a/packages/integrations/package.json b/packages/integrations/package.json index dc64f179c932..ea1e050dad5a 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -26,7 +26,7 @@ }, "scripts": { "build": "run-p build:rollup build:types build:bundle", - "build:bundle": "bash scripts/buildBundles.sh", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && bash scripts/buildBundles.sh", "build:dev": "run-p build:rollup build:types", "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", diff --git a/packages/tracing/package.json b/packages/tracing/package.json index 8344bbdec582..25510c2a1376 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -27,7 +27,7 @@ }, "scripts": { "build": "run-p build:rollup build:types build:bundle && yarn build:extras #necessary for integration tests", - "build:bundle": "rollup --config rollup.bundle.config.js", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", "build:dev": "run-p build:rollup build:types", "build:extras": "yarn build:prepack", "build:prepack": "ts-node ../../scripts/prepack.ts --bundles", diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 8fe14cd3ac16..91470886bb2c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -46,6 +46,7 @@ export type { RequestSession, RequestSessionStatus, SessionFlusherLike, + SerializedSession, } from './session'; // eslint-disable-next-line deprecation/deprecation diff --git a/packages/vue/package.json b/packages/vue/package.json index 430d46cbac0f..e1f37eade04a 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -27,7 +27,7 @@ }, "scripts": { "build": "run-p build:rollup build:types", - "build:bundle": "rollup --config rollup.bundle.config.js", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", "build:dev": "run-p build:rollup build:types", "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index baf0f28e3bc5..2bc4a612132b 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -31,7 +31,7 @@ }, "scripts": { "build": "run-p build:rollup build:bundle build:types", - "build:bundle": "rollup --config rollup.bundle.config.js", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", "build:dev": "run-p build:rollup build:types", "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", diff --git a/scripts/ensure-bundle-deps.ts b/scripts/ensure-bundle-deps.ts new file mode 100644 index 000000000000..61e724fc0029 --- /dev/null +++ b/scripts/ensure-bundle-deps.ts @@ -0,0 +1,155 @@ +/* eslint-disable no-console */ +import * as childProcess from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as util from 'util'; + +/** + * Ensure that `build:bundle` has all of the dependencies it needs to run. Works at both the repo and package level. + */ +async function ensureBundleBuildPrereqs(options: { dependencies: string[]; maxRetries?: number }): Promise { + const { maxRetries = 12, dependencies } = options; + + const { + // The directory in which the yarn command was originally invoked (which won't necessarily be the same as + // `process.cwd()`) + INIT_CWD: yarnInitialDir, + // JSON containing the args passed to `yarn` + npm_config_argv: yarnArgJSON, + } = process.env; + + if (!yarnInitialDir || !yarnArgJSON) { + const received = { INIT_CWD: yarnInitialDir, npm_config_argv: yarnArgJSON }; + throw new Error( + `Missing environment variables needed for ensuring bundle dependencies. Received:\n${util.inspect(received)}\n`, + ); + } + + // Did this build get invoked by a repo-level script, or a package-level script, and which script was it? + const isTopLevelBuild = path.basename(yarnInitialDir) === 'sentry-javascript'; + const yarnScript = (JSON.parse(yarnArgJSON) as { original: string[] }).original[0]; + + // convert '@sentry/xyz` to `xyz` + const dependencyDirs = dependencies.map(npmPackageName => npmPackageName.split('/')[1]); + + // The second half of the conditional tests if this script is being run by the original top-level command or a + // package-level command spawned by it. + const packagesDir = isTopLevelBuild && yarnInitialDir === process.cwd() ? 'packages' : '..'; + + if (checkForBundleDeps(packagesDir, dependencyDirs)) { + // We're good, nothing to do, the files we need are there + return; + } + + // If we get here, the at least some of the dependencies are missing, but how we handle that depends on how we got + // here. There are six possibilities: + // - We ran `build` or `build:bundle` at the repo level + // - We ran `build` or `build:bundle` at the package level + // - We ran `build` or `build:bundle` at the repo level and lerna then ran `build:bundle` at the package level. (We + // shouldn't ever land here under this scenario - the top-level build should already have handled any missing + // dependencies - but it's helpful to consider all the possibilities.) + // + // In the first version of the first scenario (repo-level `build` -> repo-level `build:bundle`), all we have to do is + // wait, because other parts of `build` are creating them as this check is being done. (Waiting 5 or 10 or even 15 + // seconds to start running `build:bundle` in parallel is better than pushing it to the second half of `build`, + // because `build:bundle` is the slowest part of the build and therefore the one we most want to parallelize with + // other slow parts, like `build:types`.) + // + // In all other scenarios, if the dependencies are missing, we have to build them ourselves - with `build:bundle` at + // either level, we're the only thing happening (so no one's going to do it for us), and with package-level `build`, + // types and npm assets are being built simultaneously, but only for the package being bundled, not for its + // dependencies. Either way, it's on us to fix the problem. + // + // TODO: This actually *doesn't* work for package-level `build`, not because of a flaw in this logic, but because + // `build:rollup` has similar dependency needs (it needs types rather than npm builds). We should do something like + // this for that at some point. + + if (isTopLevelBuild && yarnScript === 'build') { + let retries = 0; + + console.log('\nSearching for bundle dependencies...'); + + while (retries < maxRetries && !checkForBundleDeps(packagesDir, dependencyDirs)) { + console.log('Bundle dependencies not found. Trying again in 5 seconds.'); + retries += 1; + await sleep(5000); + } + + if (retries === maxRetries) { + throw new Error( + `\nERROR: \`yarn build:bundle\` (triggered by \`yarn build\`) cannot find its depdendencies, despite waiting ${ + 5 * maxRetries + } seconds for the rest of \`yarn build\` to create them. Something is wrong - it shouldn't take that long. Exiting.`, + ); + } + + console.log(`\nFound all bundle dependencies after ${retries} retries. Beginning bundle build...`); + } + + // top-level `build:bundle`, package-level `build` and `build:bundle` + else { + console.warn('\nWARNING: Missing dependencies for bundle build. They will be built before continuing.'); + + for (const dependencyDir of dependencyDirs) { + console.log(`\nBuilding \`${dependencyDir}\` package...`); + run('yarn build:rollup', { cwd: `${packagesDir}/${dependencyDir}` }); + } + + console.log('\nAll dependencies built successfully. Beginning bundle build...'); + } +} + +/** + * See if all of the necessary dependencies exist + */ +function checkForBundleDeps(packagesDir: string, dependencyDirs: string[]): boolean { + for (const dependencyDir of dependencyDirs) { + const depBuildDir = `${packagesDir}/${dependencyDir}/build`; + + // Checking that the directories exist 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. + if ( + !( + (fs.existsSync(`${depBuildDir}/cjs`) && fs.existsSync(`${depBuildDir}/esm`)) || + (fs.existsSync(`${depBuildDir}/npm/cjs`) && fs.existsSync(`${depBuildDir}/npm/esm`)) + ) + ) { + // Fail fast + return false; + } + } + + return true; +} + +/** + * Wait the given number of milliseconds before continuing. + */ +async function sleep(ms: number): Promise { + await new Promise(resolve => + setTimeout(() => { + resolve(); + }, ms), + ); +} + +/** + * Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current + * process. Returns contents of `stdout`. + */ +function run(cmd: string, options?: childProcess.ExecSyncOptions): string { + return String(childProcess.execSync(cmd, { stdio: 'inherit', ...options })); +} + +// 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 +// more work to get from the repo level. Fortunately this list is unlikely to change very often, and we're the only ones +// we'll break if it gets out of date. +const dependencies = ['@sentry/utils', '@sentry/hub', '@sentry/core']; + +if (['sentry-javascript', 'tracing', 'wasm'].includes(path.basename(process.cwd()))) { + dependencies.push('@sentry/browser'); +} + +void ensureBundleBuildPrereqs({ + dependencies, +});