diff --git a/client/config/webpack.config.prod.js b/client/config/webpack.config.prod.js index 6649e31e..15a1e29e 100644 --- a/client/config/webpack.config.prod.js +++ b/client/config/webpack.config.prod.js @@ -118,7 +118,7 @@ module.exports = { // We don't currently advertise code splitting but Webpack supports it. filename: 'static/js/[name].[chunkhash:8].js', chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js', - assetModuleFilename: 'static/media/[name].[hash:8].[ext]', + assetModuleFilename: 'static/media/[name].[hash:8][ext]', // Point sourcemap entries to original disk location (format as URL on Windows) devtoolModuleFilenameTemplate: (info) => path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, '/'), }, @@ -147,7 +147,7 @@ module.exports = { new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 10000, }), - new WebpackBar(), + new WebpackBar({name: 'client'}), ], optimization: { minimize: true, diff --git a/package-lock.json b/package-lock.json index 5a87ad65..edcd876b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "@types/tar-fs": "^2.0.1", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", - "@vercel/ncc": "^0.32.0", + "@vercel/webpack-asset-relocator-loader": "^1.7.0", "autoprefixer": "^10.4.0", "axios": "^0.24.0", "axios-mock-adapter": "^1.20.0", @@ -154,6 +154,7 @@ "terser-webpack-plugin": "^5.2.5", "tldts": "^5.7.53", "ts-jest": "^27.0.7", + "ts-loader": "^9.2.6", "ts-node-dev": "^1.1.8", "tsconfig-paths": "^3.12.0", "typed-emitter": "^1.4.0", @@ -4489,14 +4490,11 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@vercel/ncc": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.32.0.tgz", - "integrity": "sha512-S/SxTHHTbBQSOutpgnqEn+LyTfZcq9xMRAnzY05HpGVjxjmfmvg6SWZZkbW/GJIFznMmHGeGOrI1MEBD7efIkA==", - "dev": true, - "bin": { - "ncc": "dist/ncc/cli.js" - } + "node_modules/@vercel/webpack-asset-relocator-loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@vercel/webpack-asset-relocator-loader/-/webpack-asset-relocator-loader-1.7.0.tgz", + "integrity": "sha512-1Dy3BdOliDwxA7VZSIg55E1d/us2KvsCQOZV25fgufG//CsnZBGiSAL7qewTQf7YVHH0A9PHgzwMmKIZ8aFYVw==", + "dev": true }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", @@ -17752,6 +17750,62 @@ "node": ">=10" } }, + "node_modules/ts-loader": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.6.tgz", + "integrity": "sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/enhanced-resolve": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", + "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ts-node": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", @@ -22271,10 +22325,10 @@ } } }, - "@vercel/ncc": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.32.0.tgz", - "integrity": "sha512-S/SxTHHTbBQSOutpgnqEn+LyTfZcq9xMRAnzY05HpGVjxjmfmvg6SWZZkbW/GJIFznMmHGeGOrI1MEBD7efIkA==", + "@vercel/webpack-asset-relocator-loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@vercel/webpack-asset-relocator-loader/-/webpack-asset-relocator-loader-1.7.0.tgz", + "integrity": "sha512-1Dy3BdOliDwxA7VZSIg55E1d/us2KvsCQOZV25fgufG//CsnZBGiSAL7qewTQf7YVHH0A9PHgzwMmKIZ8aFYVw==", "dev": true }, "@webassemblyjs/ast": { @@ -32377,6 +32431,45 @@ } } }, + "ts-loader": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.6.tgz", + "integrity": "sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "dependencies": { + "enhanced-resolve": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", + "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + } + } + }, "ts-node": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", diff --git a/package.json b/package.json index 60616799..8ff7e37b 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,7 @@ ] }, "scripts": { - "build": "npm run build-assets && npm run build-ts", - "build-assets": "node client/scripts/build.js", - "build-ts": "ncc build server/bin/start.ts -m -t -e geoip-country", + "build": "node scripts/build.js", "build-pkg": "rm -rf dist && npm run build && pkg . --out-path dist-pkg", "format-source": "prettier -w .", "check-source-formatting": "prettier -c .", @@ -116,7 +114,7 @@ "@types/tar-fs": "^2.0.1", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", - "@vercel/ncc": "^0.32.0", + "@vercel/webpack-asset-relocator-loader": "^1.7.0", "autoprefixer": "^10.4.0", "axios": "^0.24.0", "axios-mock-adapter": "^1.20.0", @@ -205,6 +203,7 @@ "terser-webpack-plugin": "^5.2.5", "tldts": "^5.7.53", "ts-jest": "^27.0.7", + "ts-loader": "^9.2.6", "ts-node-dev": "^1.1.8", "tsconfig-paths": "^3.12.0", "typed-emitter": "^1.4.0", diff --git a/client/scripts/build.js b/scripts/build.js similarity index 81% rename from client/scripts/build.js rename to scripts/build.js index 1b32237b..929bdcab 100644 --- a/client/scripts/build.js +++ b/scripts/build.js @@ -14,8 +14,9 @@ const fs = require('fs-extra'); const webpack = require('webpack'); const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); -const paths = require('../../shared/config/paths'); -const config = require('../config/webpack.config.prod'); +const paths = require('../shared/config/paths'); +const clientConfig = require('../client/config/webpack.config.prod'); +const serverConfig = require('../server/config/webpack.config.prod'); const {measureFileSizesBeforeBuild, printFileSizesAfterBuild} = FileSizeReporter; @@ -39,7 +40,7 @@ const copyPublicFolder = () => { const build = (previousFileSizes) => { console.log('Creating an optimized production build...'); - const compiler = webpack(config); + const compiler = webpack([clientConfig, serverConfig]); return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err) { @@ -60,7 +61,7 @@ measureFileSizesBeforeBuild(paths.appBuild) .then((previousFileSizes) => { // Remove all content but keep the directory so that // if you're in it, you don't end up in Trash - fs.emptyDirSync(paths.appBuild); + fs.emptyDirSync(paths.dist); // Merge with the public folder copyPublicFolder(); // Start the webpack build @@ -68,25 +69,20 @@ measureFileSizesBeforeBuild(paths.appBuild) }) .then( ({stats, previousFileSizes}) => { - if (stats.hasErrors()) { - stats.compilation.errors.forEach((err) => { - console.error(err); - }); + console.log( + stats.toString({ + chunks: false, + colors: true, + }), + ); + if (stats.hasErrors()) { process.exit(1); } - if (stats.hasWarnings()) { - console.log(chalk.yellow('Compiled with warnings.\n')); - - stats.compilation.warnings.forEach((warning) => { - console.warn(warning); - }); - } - - console.log('File sizes after gzip:\n'); + console.log('\nClient file sizes after gzip:\n'); printFileSizesAfterBuild( - stats, + stats.stats[0], previousFileSizes, paths.appBuild, WARN_AFTER_BUNDLE_GZIP_SIZE, diff --git a/server/bin/start.ts b/server/bin/start.ts index 80b58d3b..9427d854 100755 --- a/server/bin/start.ts +++ b/server/bin/start.ts @@ -5,10 +5,7 @@ import chalk from 'chalk'; import enforcePrerequisites from './enforce-prerequisites'; import migrateData from './migrations/run'; -if (process.env.NODE_ENV !== 'development') { - // Use production mode by default - process.env.NODE_ENV = 'production'; - +if (process.env.NODE_ENV == 'production') { // Catch unhandled rejections and exceptions // Traces are pretty useless with minimized production codes // This avoids printing a large section of junk diff --git a/server/config/webpack.config.prod.js b/server/config/webpack.config.prod.js new file mode 100644 index 00000000..95289741 --- /dev/null +++ b/server/config/webpack.config.prod.js @@ -0,0 +1,73 @@ +const path = require('path'); +const TerserPlugin = require('terser-webpack-plugin'); +const WebpackBar = require('webpackbar'); + +const paths = require('../../shared/config/paths'); + +// Assert this just to be safe. +if (process.env.NODE_ENV !== 'production') { + throw new Error('Production builds must have NODE_ENV=production.'); +} + +module.exports = { + mode: 'production', + entry: path.resolve(__dirname, '../bin/start.ts'), + externals: { + 'geoip-country': 'node-commonjs geoip-country', + }, + resolve: { + extensions: ['.cjs', '.mjs', '.js', '.ts', '.json'], + alias: { + '@server': path.resolve(__dirname, '..'), + '@shared': path.resolve(__dirname, '../../shared'), + }, + }, + module: { + rules: [ + { + test: /\.ts$/, + loader: 'ts-loader', + exclude: /node_modules/, + options: { + context: path.resolve(__dirname, '..'), + }, + }, + { + test: /\.m?js$/, + parser: {amd: false}, + use: { + loader: '@vercel/webpack-asset-relocator-loader', + options: { + outputAssetBase: 'data', + production: true, // optional, default is undefined + }, + }, + }, + ], + }, + output: { + path: paths.dist, + filename: 'index.js', + libraryTarget: 'commonjs2', + }, + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + terserOptions: { + output: { + comments: false, + }, + }, + extractComments: false, + }), + ], + }, + plugins: [new WebpackBar({name: 'server'})], + target: 'node', + ignoreWarnings: [ + { + module: /node_modules\/yargs/, + }, + ], +}; diff --git a/shared/config/paths.d.ts b/shared/config/paths.d.ts index c58dc41a..27fa63f1 100644 --- a/shared/config/paths.d.ts +++ b/shared/config/paths.d.ts @@ -4,11 +4,8 @@ declare const PATHS: { appPublic: string; appHtml: string; appIndex: string; - appPackageJson: string; appSrc: string; - clientSrc: string; - testsSetup: string; - appNodeModules: string; + dist: string; }; export = PATHS; diff --git a/shared/config/paths.js b/shared/config/paths.js index 231b0c7d..727a2715 100644 --- a/shared/config/paths.js +++ b/shared/config/paths.js @@ -31,11 +31,8 @@ const PATHS = { appPublic: resolveApp('client/src/public/'), appHtml: resolveApp('client/src/index.html'), appIndex: resolveApp('client/src/javascript/app.tsx'), - appPackageJson: resolveApp('package.json'), appSrc: resolveApp('./'), - clientSrc: resolveApp('client/src'), - testsSetup: resolveApp('tests/setupTests.js'), - appNodeModules: resolveApp('node_modules'), + dist: resolveApp('dist'), }; module.exports = PATHS;