diff --git a/packages/cli/package.json b/packages/cli/package.json index 623b042142..0c19c7f7dd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,7 @@ "@rollup/plugin-node-resolve": "^7.1.1", "@spotify/web-scripts": "^6.0.0", "@sucrase/webpack-loader": "^2.0.0", + "bfj": "^7.0.2", "chalk": "^4.0.0", "chokidar": "^3.3.1", "commander": "^4.1.1", diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index 362f79a7fa..8b104d353c 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { run } from '../../lib/run'; +import { buildBundle } from '../../lib/bundle'; +import { Command } from 'commander'; -export default async () => { - const args = ['build']; - - await run('react-scripts', args, { - env: { - EXTEND_ESLINT: 'true', - SKIP_PREFLIGHT_CHECK: 'true', - }, +export default async (cmd: Command) => { + await buildBundle({ + entry: 'src/index', + statsJsonEnabled: cmd.stats, }); + + // Wait for interrupt signal + await new Promise(() => {}); }; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 9ed6361ba8..0e3cfaacef 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -30,6 +30,7 @@ const main = (argv: string[]) => { program .command('app:build') .description('Build an app for a production release') + .option('--stats', 'Write bundle stats to output directory') .action(actionHandler(() => require('./commands/app/build'))); program diff --git a/packages/cli/src/lib/bundle/bundle.ts b/packages/cli/src/lib/bundle/bundle.ts new file mode 100644 index 0000000000..1f2d3f214b --- /dev/null +++ b/packages/cli/src/lib/bundle/bundle.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yn from 'yn'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import webpack from 'webpack'; +import { + measureFileSizesBeforeBuild, + printFileSizesAfterBuild, +} from 'react-dev-utils/FileSizeReporter'; +import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages'; +import { createConfig } from './config'; +import { BuildOptions } from './types'; +import { resolveBundlingPaths } from './paths'; +import chalk from 'chalk'; + +// TODO(Rugvip): Limits from CRA, we might want to tweak these though. +const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; +const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; + +export async function buildBundle(options: BuildOptions) { + const { statsJsonEnabled } = options; + + const paths = resolveBundlingPaths(options); + const config = createConfig(paths, { ...options, checksEnabled: false }); + const compiler = webpack(config); + + const isCi = yn(process.env.CI, { default: false }); + + const previousFileSizes = await measureFileSizesBeforeBuild(paths.targetDist); + await fs.emptyDir(paths.targetDist); + + const { stats } = await build(compiler, isCi).catch(error => { + console.log(chalk.red('Failed to compile.\n')); + throw new Error(`Failed to compile.\n${error.message || error}`); + }); + + if (statsJsonEnabled) { + // No @types/bfj + await require('bfj').write( + resolvePath(paths.targetDist, 'bundle-stats.json'), + stats.toJson(), + ); + } + + printFileSizesAfterBuild( + stats, + previousFileSizes, + paths.targetDist, + WARN_AFTER_BUNDLE_GZIP_SIZE, + WARN_AFTER_CHUNK_GZIP_SIZE, + ); +} + +async function build(compiler: webpack.Compiler, isCi: boolean) { + const stats = await new Promise((resolve, reject) => { + compiler.run((err, buildStats) => { + if (err) { + if (err.message) { + const { errors } = formatWebpackMessages({ + errors: [err.message], + warnings: new Array(), + } as webpack.Stats.ToJsonOutput); + + throw new Error(errors[0]); + } else { + reject(err); + } + } else { + resolve(buildStats); + } + }); + }); + + const { errors, warnings } = formatWebpackMessages( + stats.toJson({ all: false, warnings: true, errors: true }), + ); + + if (errors.length) { + // Only keep the first error. Others are often indicative + // of the same problem, but confuse the reader with noise. + throw new Error(errors[0]); + } + if (isCi && warnings.length) { + console.log( + chalk.yellow( + '\nTreating warnings as errors because process.env.CI = true.\n', + ), + ); + throw new Error(warnings.join('\n\n')); + } + + return { stats }; +} diff --git a/packages/cli/src/lib/bundle/index.ts b/packages/cli/src/lib/bundle/index.ts index 51b92483da..7a1303f980 100644 --- a/packages/cli/src/lib/bundle/index.ts +++ b/packages/cli/src/lib/bundle/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { bundle } from './bundle'; +export { buildBundle } from './bundle'; export { startDevServer } from './server'; diff --git a/packages/cli/src/lib/bundle/server.ts b/packages/cli/src/lib/bundle/server.ts index 40bd5be823..907c52ecdf 100644 --- a/packages/cli/src/lib/bundle/server.ts +++ b/packages/cli/src/lib/bundle/server.ts @@ -20,10 +20,10 @@ import WebpackDevServer from 'webpack-dev-server'; import openBrowser from 'react-dev-utils/openBrowser'; import { choosePort, prepareUrls } from 'react-dev-utils/WebpackDevServerUtils'; import { createConfig } from './config'; -import { BundlingOptions } from './types'; +import { ServeOptions } from './types'; import { resolveBundlingPaths } from './paths'; -export async function startDevServer(options: BundlingOptions) { +export async function startDevServer(options: ServeOptions) { const host = process.env.HOST ?? '0.0.0.0'; const defaultPort = parseInt(process.env.PORT ?? '', 10) || 3000; diff --git a/packages/cli/src/lib/bundle/types.ts b/packages/cli/src/lib/bundle/types.ts index d5e5a0a96a..44abb47cb9 100644 --- a/packages/cli/src/lib/bundle/types.ts +++ b/packages/cli/src/lib/bundle/types.ts @@ -16,6 +16,14 @@ import { BundlingPathsOptions } from './paths'; -export type BundlingOptions = BundlingPathsOptions & { +export type BundlingOptions = { checksEnabled: boolean; }; + +export type ServeOptions = BundlingPathsOptions & { + checksEnabled: boolean; +}; + +export type BuildOptions = BundlingPathsOptions & { + statsJsonEnabled: boolean; +}; diff --git a/yarn.lock b/yarn.lock index 0bfef7c122..feae845c4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6105,6 +6105,16 @@ before-after-hook@^2.0.0, before-after-hook@^2.1.0: resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== +bfj@^7.0.2: + version "7.0.2" + resolved "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" + integrity sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw== + dependencies: + bluebird "^3.5.5" + check-types "^11.1.1" + hoopy "^0.1.4" + tryer "^1.0.1" + big.js@^3.1.3: version "3.2.0" resolved "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" @@ -6741,6 +6751,11 @@ check-more-types@2.24.0: resolved "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= +check-types@^11.1.1: + version "11.1.2" + resolved "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" + integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== + chokidar@^2.0.4, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -11091,6 +11106,11 @@ hook-std@^2.0.0: resolved "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz#ff9aafdebb6a989a354f729bb6445cf4a3a7077c" integrity sha512-zZ6T5WcuBMIUVh49iPQS9t977t7C0l7OtHrpeMb5uk48JdflRX0NSFvCekfYNmGQETnLq9W/isMyHl69kxGi8g== +hoopy@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" + integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== + hosted-git-info@^2.1.4, hosted-git-info@^2.7.1, hosted-git-info@^2.8.8: version "2.8.8" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" @@ -20983,6 +21003,11 @@ trough@^1.0.0: resolved "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +tryer@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" + integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== + ts-dedent@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/ts-dedent/-/ts-dedent-1.1.1.tgz#68fad040d7dbd53a90f545b450702340e17d18f3"