import { getProjectPath, getBuildPath } from '@erect/core/paths'; import webpack from 'webpack'; import { resolve as pathResolve, dirname } from 'path'; import md5 from 'md5'; import * as fs from 'fs-extra'; import { logger } from '@spine/logger'; import { config, AppClientBundle } from '@erect/core/config/index'; import { notify } from '@erect/server/utils/notify'; import { webpackConfigEnv, getBundle, WebpackFactory } from './configFactory'; import { BuildOptionsHookParams } from '../hooks/buildOptions'; import { createBuildOptions } from './createBuildOptions'; export const log = logger('erect:vendor'); function getVendorBundle(target: string) { const bundleConfig: AppClientBundle = getBundle(target); if (!usesVendorDll(bundleConfig)) { throw new Error(`Bundle "${target}" do not support vendor DLL`); } return bundleConfig; } export function usesVendorDll(bundleConfig: any) { return 'vendorDll' in bundleConfig && bundleConfig.vendorDll.enabled; } export function registerVendorDll(target: string, webpackFactory: WebpackFactory) { webpackFactory.register((webpackConfig) => { const bundleConfig = getVendorBundle(target); const { vendorDll } = bundleConfig; if (webpackConfig.plugins == null) { webpackConfig.plugins = []; } webpackConfig.plugins.push( new webpack.DllReferencePlugin({ // $FlowFixMe manifest: require( pathResolve( getBuildPath(), bundleConfig.buildOutputDir, `${vendorDll.fileName}.json`, ), ), } as any), ); return webpackConfig; }); } export async function buildVendorDll(buildOptions: BuildOptionsHookParams) { try { const { target, development } = buildOptions; const bundleConfig = getVendorBundle(target); const { vendorDll } = bundleConfig; const isDev = buildOptions.development; // $FlowFixMe const pkg = require(pathResolve(getProjectPath(), config.packageJson)); const isSourceProject = getProjectPath() === pathResolve(__dirname, '../../..'); const devDLLDependencies: string[] = vendorDll.include.filter((value) => { return !isSourceProject || !value.match(/^@erect\/.*/) }).slice(0).sort(); // We calculate a hash of the package.json's dependencies, which we can use // to determine if dependencies have changed since the last time we built // the vendor dll. const dependencies = pkg.dependencies == null ? {} : pkg.dependencies; const devDependencies = pkg.devDependencies == null ? {} : pkg.devDependencies; const currentDependenciesHash = md5([ `${development ? 'development' : 'production'}`, JSON.stringify( devDLLDependencies.map(dep => [dep, dependencies[dep], devDependencies[dep]]), // We do this to include any possible version numbers we may have for // a dependency. If these change then our hash should too, which will // result in a new dev dll build. ), ].join('')); const vendorDLLHashFilePath = pathResolve( getBuildPath(), bundleConfig.buildOutputDir, `${vendorDll.fileName}.lock`, ); fs.mkdirpSync(dirname(vendorDLLHashFilePath)); const webpackConfigFactory = (): webpack.Configuration => { const isClient = target === 'client'; return { mode: isDev ? 'development' : 'production', devtool: isDev || config.includeSourceMapsForOptimisedClientBundle ? 'inline-source-map' : 'hidden-source-map', entry: { [vendorDll.fileName]: devDLLDependencies, }, target: isClient ? 'web' : 'node', resolve: { extensions: ['.js'], alias: {}, }, output: { path: pathResolve(getBuildPath(), bundleConfig.buildOutputDir), filename: `${vendorDll.fileName}.js`, libraryTarget: target !== 'client' ? 'commonjs2' : 'var', }, plugins: [ new webpack.EnvironmentPlugin(webpackConfigEnv(createBuildOptions({ target: 'client', development: true, }))), new webpack.DllPlugin({ context: dirname(vendorDLLHashFilePath), path: pathResolve(getBuildPath(), bundleConfig.buildOutputDir, `${vendorDll.fileName}.json`), name: vendorDll.fileName, }), ], }; } const build = () => { return new Promise((resolve, reject) => { log( 'info', `Vendor DLL build complete. The following dependencies have been included:\n\t-${devDLLDependencies.join('\n\t-')}\n`, ); const webpackConfig = webpackConfigFactory(); const vendorDLLCompiler = webpack(webpackConfig); vendorDLLCompiler.run((err, stats) => { if (err != null) { reject(err); return; } if (stats.hasErrors()) { reject(stats.compilation.errors[0]); return; } // Update the dependency hash fs.writeFileSync(vendorDLLHashFilePath, currentDependenciesHash); resolve(); }); }); } return await new Promise((resolve, reject) => { if (!fs.existsSync(vendorDLLHashFilePath)) { // builddll log( 'warn', `Generating a new "${target}" Vendor DLL for boosted performance. The Vendor DLL helps to speed up your build time workflow by reducing Webpack build times. It does this by seperating Vendor DLLs from your primary bundles, thereby allowing Webpack to ignore them when having to rebuild your code for changes. We recommend that you add all your client bundle specific dependencies to the Vendor DLL configuration (within /config).`, ); build().then(resolve).catch(reject); } else { // first check if the md5 hashes match const dependenciesHash = fs.readFileSync(vendorDLLHashFilePath, 'utf8'); const dependenciesChanged = dependenciesHash !== currentDependenciesHash; if (dependenciesChanged) { log( 'warn', `New "${target}" vendor dependencies detected. Regenerating the vendor dll...`, ); build().then(resolve).catch(reject); } else { log( 'info', `No changes to existing "${target}" vendor dependencies. Using the existing vendor dll.`, ); resolve(true); } } }); } catch (error) { notify('✖️ Unfortunately an error occured whilst trying to build the vendor dll(s) used by the development server. Please check the console for more information.'); log.error('Failed to build vendor DLL'); throw error; } }