webpack.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. const gulp = require('gulp');
  2. const gutil = require('gulp-util');
  3. const path = require('path');
  4. const os = require('os');
  5. const { readFileSync } = require('fs');
  6. const webpack = require('webpack');
  7. const HtmlWebpackPlugin = require('html-webpack-plugin');
  8. const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
  9. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  10. const WebpackDevServer = require('webpack-dev-server');
  11. const WriteFilePlugin = require('write-file-webpack-plugin');
  12. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  13. const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
  14. const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
  15. const CopyWebpackPlugin = require('copy-webpack-plugin');
  16. const OfflinePlugin = require('offline-plugin');
  17. const WebpackPwaManifest = require('webpack-pwa-manifest');
  18. const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
  19. const VueLoaderPlugin = require('vue-loader/lib/plugin');
  20. const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
  21. module.exports = function (config) {
  22. let base = path.resolve(config.projectBase);
  23. let noop = function () {};
  24. let devNoop = !config.production ? noop : undefined;
  25. let prodNoop = config.production ? noop : undefined;
  26. // We support hot module reloading only for non ssr, non prod builds.
  27. let shouldUseHMR = !config.ssr && !config.production;
  28. // We only extract css for client SSR or prod builds.
  29. let shouldExtractCss = config.ssr || config.production;
  30. let externals = {};
  31. if (!config.client) {
  32. // When building for site, we don't want any of these imports accidentally being pulled in.
  33. // Setting these to empty object strings causes the require to return an empty object.
  34. externals['client-voodoo'] = '{}';
  35. externals['sanitize-filename'] = '{}';
  36. // fs-extra and write-file-atomic is used by the client to write the localdb json file.
  37. externals['fs-extra'] = '{}';
  38. externals['write-file-atomic'] = '{}';
  39. } else {
  40. // This format sets the externals to just straight up "require('axios')" so it can pull it
  41. // directly and not pull in through webpack's build process. We need this for axios since it
  42. // treats it as a "node" project instead of "browser". It didn't work to include axios in
  43. // here, but rather just its own dependencies.
  44. externals['follow-redirects'] = 'commonjs follow-redirects';
  45. externals['is-buffer'] = 'commonjs is-buffer';
  46. // We don't want to pull client-voodoo into the build so that it can get proper paths
  47. // through variables like __dirname and such.
  48. externals['client-voodoo'] = 'commonjs client-voodoo';
  49. // fs-extra and write-file-atomic is used by the client to write the localdb json file.
  50. externals['fs-extra'] = 'commonjs fs-extra';
  51. externals['write-file-atomic'] = 'commonjs write-file-atomic';
  52. }
  53. let webpackTarget = 'web';
  54. if (config.ssr === 'server') {
  55. webpackTarget = 'node';
  56. } else if (config.client) {
  57. webpackTarget = 'node-webkit';
  58. }
  59. let libraryTarget = 'var';
  60. if (config.ssr === 'server') {
  61. libraryTarget = 'commonjs2';
  62. }
  63. let devtool = 'eval-source-map';
  64. // In production, we never want to include source maps.
  65. if (config.production) {
  66. devtool = false;
  67. } else if (config.ssr) {
  68. devtool = 'source-map';
  69. }
  70. let isInDocker = !!process.env.GAMEJOLT_IN_DOCKER;
  71. function stylesLoader(withStylusLoader) {
  72. const loaders = ['css-loader', 'postcss-loader'];
  73. if (shouldExtractCss) {
  74. if (config.ssr !== 'server') {
  75. loaders.unshift(MiniCssExtractPlugin.loader);
  76. } else {
  77. loaders.unshift('null-loader');
  78. }
  79. } else {
  80. loaders.unshift({
  81. loader: 'vue-style-loader',
  82. options: {
  83. shadowMode: false,
  84. },
  85. });
  86. }
  87. if (withStylusLoader) {
  88. loaders.push({
  89. loader: 'stylus-loader',
  90. options: {
  91. paths: ['src/'],
  92. 'resolve url': true,
  93. 'include css': true,
  94. preferPathResolver: 'webpack',
  95. },
  96. });
  97. }
  98. return loaders;
  99. }
  100. // Check for ngrok tunnels.
  101. const getTunnels = config.production
  102. ? Promise.resolve({})
  103. : new Promise(resolve => {
  104. const http = require('http');
  105. const req = http.get(
  106. 'http://' + (isInDocker ? 'hostnet' : 'localhost') + ':4040/api/tunnels',
  107. res => {
  108. if (res.statusCode !== 200) {
  109. return resolve({});
  110. }
  111. res.setEncoding('utf8');
  112. let response = '';
  113. res.on('data', data => (response += data));
  114. res.on('end', () => {
  115. try {
  116. const parsed = JSON.parse(response);
  117. const GJ_TUNNELS = {};
  118. for (let tunnel of parsed.tunnels) {
  119. switch (tunnel.name) {
  120. case 'gj-backend (http)':
  121. GJ_TUNNELS.backend = tunnel.public_url;
  122. break;
  123. case 'gj-frontend (http)':
  124. GJ_TUNNELS.frontend = tunnel.public_url;
  125. break;
  126. }
  127. }
  128. resolve(GJ_TUNNELS);
  129. } catch (_) {
  130. resolve({});
  131. }
  132. });
  133. }
  134. );
  135. req.on('error', () => resolve({}));
  136. });
  137. let webpackSectionConfigs = {};
  138. let webpackSectionTasks = [];
  139. Object.keys(config.sections).forEach(function (section) {
  140. const sectionConfig = config.sections[section];
  141. let appEntries = ['./' + section + '/main.ts'];
  142. let indexHtml = section === 'app' ? 'index.html' : section + '.html';
  143. if (shouldUseHMR) {
  144. const devServerUrl = isInDocker
  145. ? 'https://webpack.development.gamejolt.com:443'
  146. : 'http://localhost:' + config.port;
  147. appEntries.push('webpack-dev-server/client?' + devServerUrl + '/');
  148. appEntries.push('webpack/hot/dev-server');
  149. }
  150. let entry = {
  151. app: appEntries,
  152. };
  153. if (config.ssr === 'server') {
  154. entry = {
  155. server: ['./' + section + '/server.ts'],
  156. };
  157. }
  158. let publicPath = '/';
  159. // If we need to test prod ssr build locally we should comment out this bit,
  160. // otherwise it'll attempt to fetch the chunks from our cdn.
  161. if (!config.client && config.production) {
  162. publicPath = config.staticCdn + publicPath;
  163. } else if (config.client && !config.watching) {
  164. // On linux/win we put all the files in a folder called "package".
  165. if (config.platform !== 'osx') {
  166. publicPath = '/package/';
  167. }
  168. }
  169. let webAppManifest = undefined;
  170. if (config.ssr !== 'server' && !config.client && sectionConfig.webAppManifest) {
  171. webAppManifest = sectionConfig.webAppManifest;
  172. for (const icon of webAppManifest.icons) {
  173. icon.src = path.resolve(base, 'src/app/img/touch/' + icon.src);
  174. }
  175. }
  176. let hasOfflineSupport =
  177. config.ssr !== 'server' && !config.client && config.production && sectionConfig.offline;
  178. webpackSectionConfigs[section] = {
  179. mode: config.production ? 'production' : 'development',
  180. entry,
  181. target: webpackTarget,
  182. context: path.resolve(base, 'src'),
  183. node: {
  184. __filename: true,
  185. __dirname: true,
  186. },
  187. devServer: {
  188. outputPath: path.resolve(base, config.buildDir),
  189. },
  190. output: {
  191. publicPath: publicPath,
  192. path: path.resolve(base, config.buildDir),
  193. filename:
  194. config.production || config.ssr
  195. ? section + '.[name].[contenthash:8].js'
  196. : section + '.[name].js',
  197. chunkFilename:
  198. config.production || config.ssr
  199. ? section + '.[name].[contenthash:8].js'
  200. : section + '.[name].js',
  201. sourceMapFilename:
  202. config.production || config.ssr
  203. ? 'maps/[name].[contenthash:8].map'
  204. : 'maps/[name].map',
  205. libraryTarget: libraryTarget,
  206. },
  207. resolve: {
  208. extensions: ['.tsx', '.ts', '.js', '.styl', '.vue'],
  209. modules: [path.resolve(base, 'src/vendor'), 'node_modules'],
  210. alias: {
  211. // Always "app" base img.
  212. img: path.resolve(base, 'src/app/img'),
  213. styles: path.resolve(base, 'src/' + section + '/styles'),
  214. 'styles-lib': path.resolve(base, 'src/_styles/common'),
  215. common: path.resolve(base, 'src/_common'),
  216. vue$: 'vue/dist/vue.esm.js',
  217. },
  218. },
  219. externals: externals,
  220. module: {
  221. rules: [
  222. // We don't want to import firebase ever on server for now.
  223. ...(config.ssr === 'server'
  224. ? [
  225. {
  226. test: /node_modules\/@?firebase\/.+/,
  227. loader: 'null-loader',
  228. },
  229. ]
  230. : []),
  231. {
  232. test: /\.vue$/,
  233. loader: 'vue-loader',
  234. options: {
  235. compilerOptions: {
  236. whitespace: 'preserve',
  237. },
  238. transformAssetUrls: {
  239. img: 'src',
  240. 'app-theme-svg': 'src',
  241. 'app-illustration': 'src',
  242. },
  243. },
  244. },
  245. {
  246. test: /\.ts$/,
  247. exclude: /node_modules/,
  248. use: [
  249. {
  250. loader: 'cache-loader',
  251. options: {
  252. cacheDirectory: path.resolve(base, '.cache/ts-loader'),
  253. },
  254. },
  255. {
  256. loader: 'ts-loader',
  257. options: {
  258. transpileOnly: true,
  259. experimentalWatchApi: true,
  260. appendTsSuffixTo: [/\.vue$/],
  261. },
  262. },
  263. ],
  264. },
  265. {
  266. test: /\.styl(us)?$/,
  267. use: stylesLoader(true),
  268. },
  269. {
  270. test: /\.css$/,
  271. use: stylesLoader(false),
  272. },
  273. {
  274. test: /\.md$/,
  275. use: ['html-loader', 'markdown-loader'],
  276. },
  277. {
  278. test: /\.(png|jpe?g|gif|svg|ogg|mp4)(\?.*)?$/,
  279. use: [
  280. {
  281. loader: 'file-loader',
  282. options: {
  283. name: 'assets/[name].[hash:8].[ext]',
  284. },
  285. },
  286. ],
  287. exclude: /node_modules/,
  288. },
  289. {
  290. test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
  291. use: [
  292. {
  293. loader: 'file-loader',
  294. options: {
  295. name: 'assets/[name].[hash:8].[ext]',
  296. },
  297. },
  298. ],
  299. },
  300. {
  301. test: /\.json$/,
  302. resourceQuery: /file/,
  303. loader: 'file-loader',
  304. type: 'javascript/auto',
  305. options: {
  306. name: 'assets/[name].[hash:8].[ext]',
  307. },
  308. },
  309. ],
  310. },
  311. devtool,
  312. optimization:
  313. // In ssr, we only want to do chunk splitting in the client bundle,
  314. // otherwise, we only want to do chunk splitting when doing a prod build.
  315. config.ssr === 'client' || (!config.ssr && config.production)
  316. ? {
  317. splitChunks: {
  318. // Does chunk splitting logic for entry point chunks as well.
  319. // https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
  320. //
  321. // When building for ssr it fails splitting css chunks when using 'all' or 'async'.
  322. // The chunk is split correctly but isnt loaded into the initial page. instead of being included as a 'preload',
  323. // it includes it as a 'prefetch'. Not sure where the issue is, so at the moment we just resort to 'initial'.
  324. chunks: config.ssr ? 'initial' : 'all',
  325. },
  326. // Splits the runtime into its own chunk for long-term caching.
  327. runtimeChunk: 'single',
  328. }
  329. : undefined,
  330. plugins: [
  331. new VueLoaderPlugin(),
  332. prodNoop || new webpack.ProgressPlugin(),
  333. new webpack.DefinePlugin({
  334. GJ_TUNNELS: JSON.stringify({}),
  335. }),
  336. new webpack.DefinePlugin({
  337. GJ_SECTION: JSON.stringify(section),
  338. GJ_ENVIRONMENT: JSON.stringify(
  339. !config.developmentEnv ? 'production' : 'development'
  340. ),
  341. GJ_BUILD_TYPE: JSON.stringify(config.production ? 'production' : 'development'),
  342. GJ_IS_CLIENT: JSON.stringify(!!config.client),
  343. GJ_IS_SSR: JSON.stringify(config.ssr === 'server'),
  344. GJ_VERSION: JSON.stringify(
  345. require(path.resolve(process.cwd(), 'package.json')).version
  346. ),
  347. GJ_MANIFEST_URL: JSON.stringify(
  348. require(path.resolve(process.cwd(), 'package.json')).clientManifestUrl
  349. ),
  350. GJ_WITH_UPDATER: JSON.stringify(
  351. (!config.developmentEnv && !config.watching) || config.withUpdater
  352. ),
  353. GJ_IS_WATCHING: JSON.stringify(config.watching),
  354. GJ_WITH_LOCALSTOAGE_AUTH_REDIRECT: JSON.stringify(
  355. config.withLocalStorageAuthRedirect
  356. ),
  357. }),
  358. new CopyWebpackPlugin([
  359. {
  360. context: path.resolve(base, 'src/static-assets'),
  361. from: '**/*',
  362. to: 'static-assets',
  363. },
  364. ]),
  365. // Copy over stupid client stuff that's needed.
  366. !config.client
  367. ? noop
  368. : new CopyWebpackPlugin([
  369. {
  370. from: path.join(base, 'package.json'),
  371. to: 'package.json',
  372. transform: (content, _path) => {
  373. const pkg = JSON.parse(content);
  374. // We don't want to install dev/optional deps into the client build.
  375. // We only need those when building the client, not for runtime.
  376. delete pkg.devDependencies;
  377. delete pkg.optionalDependencies;
  378. delete pkg.scripts;
  379. return JSON.stringify(pkg);
  380. },
  381. },
  382. {
  383. from: 'update-hook.js',
  384. to: 'update-hook.js',
  385. },
  386. ]),
  387. devNoop ||
  388. new ImageMinimizerPlugin({
  389. minimizerOptions: {
  390. // Lossless optimization. Pulled from the ImageMinimizerPlugin readme.
  391. plugins: [
  392. ['gifsicle', { interlaced: true }],
  393. ['jpegtran', { progressive: true }],
  394. ['optipng', { optimizationLevel: 5 }],
  395. [
  396. 'svgo',
  397. {
  398. plugins: [
  399. {
  400. removeViewBox: false,
  401. },
  402. ],
  403. },
  404. ],
  405. ],
  406. },
  407. }),
  408. !shouldUseHMR ? noop : new webpack.HotModuleReplacementPlugin(),
  409. !shouldExtractCss
  410. ? noop
  411. : new MiniCssExtractPlugin({
  412. filename: section + '.[name].[contenthash:8].css',
  413. chunkFilename: section + '.[name].[contenthash:8].css',
  414. }),
  415. !shouldExtractCss
  416. ? noop
  417. : new OptimizeCssnanoPlugin({
  418. cssnanoOptions: {
  419. preset: [
  420. 'default',
  421. {
  422. mergeLonghand: false,
  423. cssDeclarationSorter: false,
  424. },
  425. ],
  426. },
  427. }),
  428. config.ssr === 'server'
  429. ? noop
  430. : new HtmlWebpackPlugin({
  431. filename: indexHtml,
  432. template: 'index.html',
  433. favicon: 'app/img/favicon.png',
  434. inject: true,
  435. chunksSortMode: 'none',
  436. // Our own vars for injection into template.
  437. templateParameters: {
  438. _section: section,
  439. _isClient: config.client,
  440. _title: sectionConfig.title,
  441. _crawl: !!sectionConfig.crawl,
  442. _scripts: sectionConfig.scripts,
  443. _bodyClass: sectionConfig.bodyClass || '',
  444. _perfPolyfill: readFileSync(
  445. path.resolve(
  446. base,
  447. 'node_modules/first-input-delay/dist/first-input-delay.min.js'
  448. )
  449. ),
  450. },
  451. }),
  452. webAppManifest ? new WebpackPwaManifest(webAppManifest) : noop,
  453. prodNoop || new FriendlyErrorsWebpackPlugin(),
  454. // Make the client bundle for both normal prod builds or client ssr builds.
  455. // We want to compare the manifests from the two builds.
  456. (config.ssr === 'client' || config.production) && !config.client
  457. ? new VueSSRClientPlugin({
  458. filename: 'vue-ssr-client-manifest-' + section + '.json',
  459. })
  460. : noop,
  461. config.ssr === 'server' && !config.client
  462. ? new VueSSRServerPlugin({
  463. filename: 'vue-ssr-server-bundle-' + section + '.json',
  464. })
  465. : noop,
  466. hasOfflineSupport
  467. ? new OfflinePlugin({
  468. excludes: ['**/.*', '**/*.map', 'vue-ssr-*', '**/*gameApiDocContent*'],
  469. ServiceWorker: {
  470. events: true,
  471. output: 'sjw.js',
  472. publicPath: 'https://gamejolt.com/sjw.js',
  473. },
  474. })
  475. : noop,
  476. devNoop || new webpack.HashedModuleIdsPlugin(),
  477. config.write ? new WriteFilePlugin() : noop,
  478. config.analyzeBundle ? new BundleAnalyzerPlugin({ openAnalyzer: true }) : noop,
  479. ],
  480. };
  481. gulp.task('compile:' + section, function (cb) {
  482. let compiler = webpack(webpackSectionConfigs[section]);
  483. compiler.run(function (err, stats) {
  484. if (err) {
  485. throw new gutil.PluginError('webpack:build', err);
  486. }
  487. gutil.log(
  488. '[webpack:build]',
  489. stats.toString({
  490. chunks: false,
  491. colors: true,
  492. })
  493. );
  494. cb();
  495. });
  496. });
  497. webpackSectionTasks.push('compile:' + section);
  498. });
  499. gulp.task(
  500. 'watch',
  501. gulp.series('clean', function (cb) {
  502. const buildSections = config.buildSection.split(',');
  503. let port = parseInt(config.port),
  504. portOffset = 0;
  505. getTunnels
  506. .then(GJ_TUNNELS => {
  507. console.log(GJ_TUNNELS);
  508. for (let buildSection of buildSections) {
  509. // Insert another define plugin with the GJ_TUNNELS const.
  510. const sectionConfig = webpackSectionConfigs[buildSection];
  511. sectionConfig.plugins.splice(
  512. 2, // replace the default GJ_TUNNELS define plugin.
  513. 1,
  514. new webpack.DefinePlugin({
  515. GJ_TUNNELS: JSON.stringify(GJ_TUNNELS),
  516. })
  517. );
  518. const hasTunnels = Object.keys(GJ_TUNNELS).length > 0;
  519. console.log('watching ' + buildSection + ' on port ' + (port + portOffset));
  520. let compiler = webpack(sectionConfig);
  521. let server = new WebpackDevServer(compiler, {
  522. historyApiFallback: {
  523. rewrites: [
  524. {
  525. from: /./,
  526. to:
  527. buildSection === 'app'
  528. ? '/index.html'
  529. : '/' + buildSection + '.html',
  530. },
  531. ],
  532. },
  533. public: isInDocker
  534. ? 'webpack.development.gamejolt.com'
  535. : 'localhost:' + config.port,
  536. transportMode: 'ws',
  537. // quiet: true,
  538. progress: true,
  539. disableHostCheck: true,
  540. compress: hasTunnels,
  541. hot: shouldUseHMR,
  542. watchOptions: {
  543. ignored: /node_modules/,
  544. poll: 300,
  545. },
  546. });
  547. if (config.ssr !== 'server') {
  548. server.listen(port + portOffset, isInDocker ? '0.0.0.0' : '127.0.0.1');
  549. }
  550. portOffset += 1;
  551. }
  552. })
  553. .catch(cb);
  554. })
  555. );
  556. if (!config.noClean) {
  557. webpackSectionTasks.unshift('clean');
  558. }
  559. webpackSectionTasks.unshift('translations:compile');
  560. if (config.client && !config.watching) {
  561. webpackSectionTasks.push('client');
  562. }
  563. gulp.task('default', gulp.series(webpackSectionTasks));
  564. };