123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- const gulp = require('gulp');
- const gutil = require('gulp-util');
- const path = require('path');
- const os = require('os');
- const { readFileSync } = require('fs');
- const webpack = require('webpack');
- const HtmlWebpackPlugin = require('html-webpack-plugin');
- const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
- const MiniCssExtractPlugin = require('mini-css-extract-plugin');
- const WebpackDevServer = require('webpack-dev-server');
- const WriteFilePlugin = require('write-file-webpack-plugin');
- const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
- const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
- const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
- const CopyWebpackPlugin = require('copy-webpack-plugin');
- const OfflinePlugin = require('offline-plugin');
- const WebpackPwaManifest = require('webpack-pwa-manifest');
- const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
- const VueLoaderPlugin = require('vue-loader/lib/plugin');
- const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
- module.exports = function (config) {
- let base = path.resolve(config.projectBase);
- let noop = function () {};
- let devNoop = !config.production ? noop : undefined;
- let prodNoop = config.production ? noop : undefined;
- // We support hot module reloading only for non ssr, non prod builds.
- let shouldUseHMR = !config.ssr && !config.production;
- // We only extract css for client SSR or prod builds.
- let shouldExtractCss = config.ssr || config.production;
- let externals = {};
- if (!config.client) {
- // When building for site, we don't want any of these imports accidentally being pulled in.
- // Setting these to empty object strings causes the require to return an empty object.
- externals['client-voodoo'] = '{}';
- externals['sanitize-filename'] = '{}';
- // fs-extra and write-file-atomic is used by the client to write the localdb json file.
- externals['fs-extra'] = '{}';
- externals['write-file-atomic'] = '{}';
- } else {
- // This format sets the externals to just straight up "require('axios')" so it can pull it
- // directly and not pull in through webpack's build process. We need this for axios since it
- // treats it as a "node" project instead of "browser". It didn't work to include axios in
- // here, but rather just its own dependencies.
- externals['follow-redirects'] = 'commonjs follow-redirects';
- externals['is-buffer'] = 'commonjs is-buffer';
- // We don't want to pull client-voodoo into the build so that it can get proper paths
- // through variables like __dirname and such.
- externals['client-voodoo'] = 'commonjs client-voodoo';
- // fs-extra and write-file-atomic is used by the client to write the localdb json file.
- externals['fs-extra'] = 'commonjs fs-extra';
- externals['write-file-atomic'] = 'commonjs write-file-atomic';
- }
- let webpackTarget = 'web';
- if (config.ssr === 'server') {
- webpackTarget = 'node';
- } else if (config.client) {
- webpackTarget = 'node-webkit';
- }
- let libraryTarget = 'var';
- if (config.ssr === 'server') {
- libraryTarget = 'commonjs2';
- }
- let devtool = 'eval-source-map';
- // In production, we never want to include source maps.
- if (config.production) {
- devtool = false;
- } else if (config.ssr) {
- devtool = 'source-map';
- }
- let isInDocker = !!process.env.GAMEJOLT_IN_DOCKER;
- function stylesLoader(withStylusLoader) {
- const loaders = ['css-loader', 'postcss-loader'];
- if (shouldExtractCss) {
- if (config.ssr !== 'server') {
- loaders.unshift(MiniCssExtractPlugin.loader);
- } else {
- loaders.unshift('null-loader');
- }
- } else {
- loaders.unshift({
- loader: 'vue-style-loader',
- options: {
- shadowMode: false,
- },
- });
- }
- if (withStylusLoader) {
- loaders.push({
- loader: 'stylus-loader',
- options: {
- paths: ['src/'],
- 'resolve url': true,
- 'include css': true,
- preferPathResolver: 'webpack',
- },
- });
- }
- return loaders;
- }
- // Check for ngrok tunnels.
- const getTunnels = config.production
- ? Promise.resolve({})
- : new Promise(resolve => {
- const http = require('http');
- const req = http.get(
- 'http://' + (isInDocker ? 'hostnet' : 'localhost') + ':4040/api/tunnels',
- res => {
- if (res.statusCode !== 200) {
- return resolve({});
- }
- res.setEncoding('utf8');
- let response = '';
- res.on('data', data => (response += data));
- res.on('end', () => {
- try {
- const parsed = JSON.parse(response);
- const GJ_TUNNELS = {};
- for (let tunnel of parsed.tunnels) {
- switch (tunnel.name) {
- case 'gj-backend (http)':
- GJ_TUNNELS.backend = tunnel.public_url;
- break;
- case 'gj-frontend (http)':
- GJ_TUNNELS.frontend = tunnel.public_url;
- break;
- }
- }
- resolve(GJ_TUNNELS);
- } catch (_) {
- resolve({});
- }
- });
- }
- );
- req.on('error', () => resolve({}));
- });
- let webpackSectionConfigs = {};
- let webpackSectionTasks = [];
- Object.keys(config.sections).forEach(function (section) {
- const sectionConfig = config.sections[section];
- let appEntries = ['./' + section + '/main.ts'];
- let indexHtml = section === 'app' ? 'index.html' : section + '.html';
- if (shouldUseHMR) {
- const devServerUrl = isInDocker
- ? 'https://webpack.development.gamejolt.com:443'
- : 'http://localhost:' + config.port;
- appEntries.push('webpack-dev-server/client?' + devServerUrl + '/');
- appEntries.push('webpack/hot/dev-server');
- }
- let entry = {
- app: appEntries,
- };
- if (config.ssr === 'server') {
- entry = {
- server: ['./' + section + '/server.ts'],
- };
- }
- let publicPath = '/';
- // If we need to test prod ssr build locally we should comment out this bit,
- // otherwise it'll attempt to fetch the chunks from our cdn.
- if (!config.client && config.production) {
- publicPath = config.staticCdn + publicPath;
- } else if (config.client && !config.watching) {
- // On linux/win we put all the files in a folder called "package".
- if (config.platform !== 'osx') {
- publicPath = '/package/';
- }
- }
- let webAppManifest = undefined;
- if (config.ssr !== 'server' && !config.client && sectionConfig.webAppManifest) {
- webAppManifest = sectionConfig.webAppManifest;
- for (const icon of webAppManifest.icons) {
- icon.src = path.resolve(base, 'src/app/img/touch/' + icon.src);
- }
- }
- let hasOfflineSupport =
- config.ssr !== 'server' && !config.client && config.production && sectionConfig.offline;
- webpackSectionConfigs[section] = {
- mode: config.production ? 'production' : 'development',
- entry,
- target: webpackTarget,
- context: path.resolve(base, 'src'),
- node: {
- __filename: true,
- __dirname: true,
- },
- devServer: {
- outputPath: path.resolve(base, config.buildDir),
- },
- output: {
- publicPath: publicPath,
- path: path.resolve(base, config.buildDir),
- filename:
- config.production || config.ssr
- ? section + '.[name].[contenthash:8].js'
- : section + '.[name].js',
- chunkFilename:
- config.production || config.ssr
- ? section + '.[name].[contenthash:8].js'
- : section + '.[name].js',
- sourceMapFilename:
- config.production || config.ssr
- ? 'maps/[name].[contenthash:8].map'
- : 'maps/[name].map',
- libraryTarget: libraryTarget,
- },
- resolve: {
- extensions: ['.tsx', '.ts', '.js', '.styl', '.vue'],
- modules: [path.resolve(base, 'src/vendor'), 'node_modules'],
- alias: {
- // Always "app" base img.
- img: path.resolve(base, 'src/app/img'),
- styles: path.resolve(base, 'src/' + section + '/styles'),
- 'styles-lib': path.resolve(base, 'src/_styles/common'),
- common: path.resolve(base, 'src/_common'),
- vue$: 'vue/dist/vue.esm.js',
- },
- },
- externals: externals,
- module: {
- rules: [
- // We don't want to import firebase ever on server for now.
- ...(config.ssr === 'server'
- ? [
- {
- test: /node_modules\/@?firebase\/.+/,
- loader: 'null-loader',
- },
- ]
- : []),
- {
- test: /\.vue$/,
- loader: 'vue-loader',
- options: {
- compilerOptions: {
- whitespace: 'preserve',
- },
- transformAssetUrls: {
- img: 'src',
- 'app-theme-svg': 'src',
- 'app-illustration': 'src',
- },
- },
- },
- {
- test: /\.ts$/,
- exclude: /node_modules/,
- use: [
- {
- loader: 'cache-loader',
- options: {
- cacheDirectory: path.resolve(base, '.cache/ts-loader'),
- },
- },
- {
- loader: 'ts-loader',
- options: {
- transpileOnly: true,
- experimentalWatchApi: true,
- appendTsSuffixTo: [/\.vue$/],
- },
- },
- ],
- },
- {
- test: /\.styl(us)?$/,
- use: stylesLoader(true),
- },
- {
- test: /\.css$/,
- use: stylesLoader(false),
- },
- {
- test: /\.md$/,
- use: ['html-loader', 'markdown-loader'],
- },
- {
- test: /\.(png|jpe?g|gif|svg|ogg|mp4)(\?.*)?$/,
- use: [
- {
- loader: 'file-loader',
- options: {
- name: 'assets/[name].[hash:8].[ext]',
- },
- },
- ],
- exclude: /node_modules/,
- },
- {
- test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
- use: [
- {
- loader: 'file-loader',
- options: {
- name: 'assets/[name].[hash:8].[ext]',
- },
- },
- ],
- },
- {
- test: /\.json$/,
- resourceQuery: /file/,
- loader: 'file-loader',
- type: 'javascript/auto',
- options: {
- name: 'assets/[name].[hash:8].[ext]',
- },
- },
- ],
- },
- devtool,
- optimization:
- // In ssr, we only want to do chunk splitting in the client bundle,
- // otherwise, we only want to do chunk splitting when doing a prod build.
- config.ssr === 'client' || (!config.ssr && config.production)
- ? {
- splitChunks: {
- // Does chunk splitting logic for entry point chunks as well.
- // https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
- //
- // When building for ssr it fails splitting css chunks when using 'all' or 'async'.
- // The chunk is split correctly but isnt loaded into the initial page. instead of being included as a 'preload',
- // it includes it as a 'prefetch'. Not sure where the issue is, so at the moment we just resort to 'initial'.
- chunks: config.ssr ? 'initial' : 'all',
- },
- // Splits the runtime into its own chunk for long-term caching.
- runtimeChunk: 'single',
- }
- : undefined,
- plugins: [
- new VueLoaderPlugin(),
- prodNoop || new webpack.ProgressPlugin(),
- new webpack.DefinePlugin({
- GJ_TUNNELS: JSON.stringify({}),
- }),
- new webpack.DefinePlugin({
- GJ_SECTION: JSON.stringify(section),
- GJ_ENVIRONMENT: JSON.stringify(
- !config.developmentEnv ? 'production' : 'development'
- ),
- GJ_BUILD_TYPE: JSON.stringify(config.production ? 'production' : 'development'),
- GJ_IS_CLIENT: JSON.stringify(!!config.client),
- GJ_IS_SSR: JSON.stringify(config.ssr === 'server'),
- GJ_VERSION: JSON.stringify(
- require(path.resolve(process.cwd(), 'package.json')).version
- ),
- GJ_MANIFEST_URL: JSON.stringify(
- require(path.resolve(process.cwd(), 'package.json')).clientManifestUrl
- ),
- GJ_WITH_UPDATER: JSON.stringify(
- (!config.developmentEnv && !config.watching) || config.withUpdater
- ),
- GJ_IS_WATCHING: JSON.stringify(config.watching),
- GJ_WITH_LOCALSTOAGE_AUTH_REDIRECT: JSON.stringify(
- config.withLocalStorageAuthRedirect
- ),
- }),
- new CopyWebpackPlugin([
- {
- context: path.resolve(base, 'src/static-assets'),
- from: '**/*',
- to: 'static-assets',
- },
- ]),
- // Copy over stupid client stuff that's needed.
- !config.client
- ? noop
- : new CopyWebpackPlugin([
- {
- from: path.join(base, 'package.json'),
- to: 'package.json',
- transform: (content, _path) => {
- const pkg = JSON.parse(content);
- // We don't want to install dev/optional deps into the client build.
- // We only need those when building the client, not for runtime.
- delete pkg.devDependencies;
- delete pkg.optionalDependencies;
- delete pkg.scripts;
- return JSON.stringify(pkg);
- },
- },
- {
- from: 'update-hook.js',
- to: 'update-hook.js',
- },
- ]),
- devNoop ||
- new ImageMinimizerPlugin({
- minimizerOptions: {
- // Lossless optimization. Pulled from the ImageMinimizerPlugin readme.
- plugins: [
- ['gifsicle', { interlaced: true }],
- ['jpegtran', { progressive: true }],
- ['optipng', { optimizationLevel: 5 }],
- [
- 'svgo',
- {
- plugins: [
- {
- removeViewBox: false,
- },
- ],
- },
- ],
- ],
- },
- }),
- !shouldUseHMR ? noop : new webpack.HotModuleReplacementPlugin(),
- !shouldExtractCss
- ? noop
- : new MiniCssExtractPlugin({
- filename: section + '.[name].[contenthash:8].css',
- chunkFilename: section + '.[name].[contenthash:8].css',
- }),
- !shouldExtractCss
- ? noop
- : new OptimizeCssnanoPlugin({
- cssnanoOptions: {
- preset: [
- 'default',
- {
- mergeLonghand: false,
- cssDeclarationSorter: false,
- },
- ],
- },
- }),
- config.ssr === 'server'
- ? noop
- : new HtmlWebpackPlugin({
- filename: indexHtml,
- template: 'index.html',
- favicon: 'app/img/favicon.png',
- inject: true,
- chunksSortMode: 'none',
- // Our own vars for injection into template.
- templateParameters: {
- _section: section,
- _isClient: config.client,
- _title: sectionConfig.title,
- _crawl: !!sectionConfig.crawl,
- _scripts: sectionConfig.scripts,
- _bodyClass: sectionConfig.bodyClass || '',
- _perfPolyfill: readFileSync(
- path.resolve(
- base,
- 'node_modules/first-input-delay/dist/first-input-delay.min.js'
- )
- ),
- },
- }),
- webAppManifest ? new WebpackPwaManifest(webAppManifest) : noop,
- prodNoop || new FriendlyErrorsWebpackPlugin(),
- // Make the client bundle for both normal prod builds or client ssr builds.
- // We want to compare the manifests from the two builds.
- (config.ssr === 'client' || config.production) && !config.client
- ? new VueSSRClientPlugin({
- filename: 'vue-ssr-client-manifest-' + section + '.json',
- })
- : noop,
- config.ssr === 'server' && !config.client
- ? new VueSSRServerPlugin({
- filename: 'vue-ssr-server-bundle-' + section + '.json',
- })
- : noop,
- hasOfflineSupport
- ? new OfflinePlugin({
- excludes: ['**/.*', '**/*.map', 'vue-ssr-*', '**/*gameApiDocContent*'],
- ServiceWorker: {
- events: true,
- output: 'sjw.js',
- publicPath: 'https://gamejolt.com/sjw.js',
- },
- })
- : noop,
- devNoop || new webpack.HashedModuleIdsPlugin(),
- config.write ? new WriteFilePlugin() : noop,
- config.analyzeBundle ? new BundleAnalyzerPlugin({ openAnalyzer: true }) : noop,
- ],
- };
- gulp.task('compile:' + section, function (cb) {
- let compiler = webpack(webpackSectionConfigs[section]);
- compiler.run(function (err, stats) {
- if (err) {
- throw new gutil.PluginError('webpack:build', err);
- }
- gutil.log(
- '[webpack:build]',
- stats.toString({
- chunks: false,
- colors: true,
- })
- );
- cb();
- });
- });
- webpackSectionTasks.push('compile:' + section);
- });
- gulp.task(
- 'watch',
- gulp.series('clean', function (cb) {
- const buildSections = config.buildSection.split(',');
- let port = parseInt(config.port),
- portOffset = 0;
- getTunnels
- .then(GJ_TUNNELS => {
- console.log(GJ_TUNNELS);
- for (let buildSection of buildSections) {
- // Insert another define plugin with the GJ_TUNNELS const.
- const sectionConfig = webpackSectionConfigs[buildSection];
- sectionConfig.plugins.splice(
- 2, // replace the default GJ_TUNNELS define plugin.
- 1,
- new webpack.DefinePlugin({
- GJ_TUNNELS: JSON.stringify(GJ_TUNNELS),
- })
- );
- const hasTunnels = Object.keys(GJ_TUNNELS).length > 0;
- console.log('watching ' + buildSection + ' on port ' + (port + portOffset));
- let compiler = webpack(sectionConfig);
- let server = new WebpackDevServer(compiler, {
- historyApiFallback: {
- rewrites: [
- {
- from: /./,
- to:
- buildSection === 'app'
- ? '/index.html'
- : '/' + buildSection + '.html',
- },
- ],
- },
- public: isInDocker
- ? 'webpack.development.gamejolt.com'
- : 'localhost:' + config.port,
- transportMode: 'ws',
- // quiet: true,
- progress: true,
- disableHostCheck: true,
- compress: hasTunnels,
- hot: shouldUseHMR,
- watchOptions: {
- ignored: /node_modules/,
- poll: 300,
- },
- });
- if (config.ssr !== 'server') {
- server.listen(port + portOffset, isInDocker ? '0.0.0.0' : '127.0.0.1');
- }
- portOffset += 1;
- }
- })
- .catch(cb);
- })
- );
- if (!config.noClean) {
- webpackSectionTasks.unshift('clean');
- }
- webpackSectionTasks.unshift('translations:compile');
- if (config.client && !config.watching) {
- webpackSectionTasks.push('client');
- }
- gulp.task('default', gulp.series(webpackSectionTasks));
- };
|