From 46bae9ca3b383e172dced2befea120bce9c5654b Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 22 Jun 2021 23:32:18 +1000 Subject: [PATCH] chore(refactor): Refactor app, improve code and stability An important fix for the way the loaders were being detected and replaced has been addressed. In this commit we're also introducing: - Logging - More types - New eslint config --- src/compatibility.ts | 11 ++-- src/constants.ts | 4 ++ src/index.ts | 76 ++++++++++++------------ src/logger.ts | 4 ++ src/optimisations/esbuild.ts | 98 +++++++++++++++---------------- src/optimisations/images.ts | 25 ++++---- src/optimisations/nuxt.ts | 52 ++++++++-------- src/optimisations/webpack.ts | 6 +- src/tools/speed-measure-plugin.ts | 26 ++++---- src/types.ts | 11 +++- 10 files changed, 166 insertions(+), 147 deletions(-) create mode 100644 src/constants.ts create mode 100644 src/logger.ts diff --git a/src/compatibility.ts b/src/compatibility.ts index 66394e4..030c251 100644 --- a/src/compatibility.ts +++ b/src/compatibility.ts @@ -1,21 +1,20 @@ - import chalk from 'chalk' import semver from 'semver' -export function requireNuxtVersion (currentVersion?: string, requiredVersion?: string) { +export function requireNuxtVersion(currentVersion?: string, requiredVersion?: string) { + // eslint-disable-next-line @typescript-eslint/no-var-requires const pkgName = require('../package.json').name - if (!currentVersion || !requireNuxtVersion) { + if (!currentVersion || !requireNuxtVersion) return - } const _currentVersion = semver.coerce(currentVersion)! const _requiredVersion = semver.coerce(requiredVersion)! if (semver.lt(_currentVersion, _requiredVersion)) { throw new Error(`\n - ${chalk.cyan(pkgName)} is not compatible with your current Nuxt version : ${chalk.yellow('v' + currentVersion)}\n - Required: ${chalk.green('v' + requiredVersion)} or ${chalk.cyan('higher')} + ${chalk.cyan(pkgName)} is not compatible with your current Nuxt version : ${chalk.yellow(`v${currentVersion}`)}\n + Required: ${chalk.green(`v${requiredVersion}`)} or ${chalk.cyan('higher')} `) } } diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..5566cab --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,4 @@ +export const NAME = 'nuxt-build-optimisations' +export const RISK_PROFILE_SAFE = 'safe' +export const RISK_PROFILE_EXPERIMENTAL = 'experimental' +export const RISK_PROFILE_RISKY = 'risky' diff --git a/src/index.ts b/src/index.ts index 287837c..87d11fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,76 +1,80 @@ import type { Module } from '@nuxt/types' import type { ExtendFunctionContext } from '@nuxt/types/config/module' -import type { Configuration as WebpackConfig } from 'webpack' +import defu from 'defu' +import { Configuration as WebpackConfiguration } from 'webpack' import type { OptimisationArgs, ModuleOptions } from './types' import { requireNuxtVersion } from './compatibility' import speedMeasurePlugin from './tools/speed-measure-plugin' import { webpackOptimiser, imageOptimiser, esbuildOptimiser, nuxtOptimiser } from './optimisations' +import logger from './logger' +import { NAME } from './constants' -const buildOptimisationsModule: Module = function () { +const buildOptimisationsModule: Module = async function(moduleOptions) { const { nuxt } = this - const defaults = { + + requireNuxtVersion(nuxt.constructor.version, '2.10') + + const defaultConfig: ModuleOptions = { measure: false, measureMode: 'client', profile: 'experimental', esbuildMinifyOptions: { - target: 'es2015' + target: 'es2015', }, esbuildLoaderOptions: { - target: 'es2015' + target: 'es2015', }, features: { + postcssNoPolyfills: true, esbuildLoader: true, esbuildMinifier: true, imageFileLoader: true, webpackOptimisations: true, cacheLoader: true, - hardSourcePlugin: true - } - } as ModuleOptions - const buildOptimisations = { - ...defaults, - ...nuxt.options.buildOptimisations - } as ModuleOptions - - requireNuxtVersion(nuxt.constructor.version, '2.10') + hardSourcePlugin: true, + parallelPlugin: true, + }, + } + const options: ModuleOptions = defu.arrayFn(moduleOptions, nuxt.options.buildOptimisations, defaultConfig) // set measure based on env if the env is set - if (typeof process.env.NUXT_MEASURE !== 'undefined') { - buildOptimisations.measure = process.env.NUXT_MEASURE.toLowerCase() === 'true' - } + if (typeof process.env.NUXT_MEASURE !== 'undefined') + options.measure = process.env.NUXT_MEASURE.toLowerCase() === 'true' + + await nuxt.callHook('buildOptimisations:options', options) + logger.debug('post `buildOptimisations:options` hook options', options) - nuxt.hook('build:before', () => { + nuxt.hook('build:before', (nuxt: any) => { const args = { - options: buildOptimisations, + options, nuxtOptions: nuxt.options, - env: { isDev: nuxt.dev || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev' } + env: { isDev: nuxt.dev || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev' }, } as OptimisationArgs - // if the user has enabled speed measure plugin and we can + + if (process.env.NODE_ENV !== 'test') + logger.info(`\`${NAME}\` enabled with \`${options.profile}\` profile.`) + + // boot speed measure plugin speedMeasurePlugin(args, nuxt) // if profile is false we don't add any optimisations - if (buildOptimisations.profile === false) { + if (options.profile === false) return - } - if (process.env.NODE_ENV !== 'test') { - console.info(`\`nuxt-build-optimisations\` enabled with \`${buildOptimisations.profile}\` profile.`) - } - // @ts-ignore + nuxtOptimiser(args) - this.extendBuild((config: WebpackConfig, env: ExtendFunctionContext) => { - args.env = env + this.extendBuild((config: WebpackConfiguration, ctx: ExtendFunctionContext) => { + args.env = ctx args.config = config - const extendOptimisers = [ - webpackOptimiser, imageOptimiser, esbuildOptimiser - ] - for (const k in extendOptimisers) { - extendOptimisers[k](args) - } + args.logger = logger.withScope(ctx.isModern ? 'modern' : (ctx.isClient ? 'client' : 'server')) + // call all of them + webpackOptimiser(args) + imageOptimiser(args) + esbuildOptimiser(args) }) }) } // @ts-ignore -buildOptimisationsModule.meta = { name: 'nuxt-build-optimisations' } +buildOptimisationsModule.meta = { name: NAME } export default buildOptimisationsModule diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..0bb9ef2 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,4 @@ +import consola from 'consola' +import { NAME } from './constants' + +export default consola.withScope(NAME) diff --git a/src/optimisations/esbuild.ts b/src/optimisations/esbuild.ts index e0abcac..c744a07 100644 --- a/src/optimisations/esbuild.ts +++ b/src/optimisations/esbuild.ts @@ -1,78 +1,74 @@ import { ESBuildMinifyPlugin } from 'esbuild-loader' +import { RuleSetUseItem } from 'webpack' import { OptimisationArgs } from '../types' +import { RISK_PROFILE_SAFE } from '../constants' -export default (args : OptimisationArgs) => { - const { options, nuxtOptions, config, env } = args - if (!config.module || !config.plugins) { +export default (args: OptimisationArgs) => { + const { options, nuxtOptions, config, env, logger } = args + if (!config.module || !config.plugins) return - } - if (options.features.esbuildLoader && (env.isDev || options.profile !== 'safe')) { + // we replace the babel loader with esbuild + if (options.features.esbuildLoader && (env.isDev || options.profile !== RISK_PROFILE_SAFE)) { const esbuildLoaderOptions = typeof options.esbuildLoaderOptions === 'function' ? options.esbuildLoaderOptions(args) : options.esbuildLoaderOptions - let cacheLoader = [] // remove the nuxt js/ts loaders - config.module.rules.forEach((rule, ruleKey) => { + config.module.rules.map((rule) => { // nuxt js / ts file matching - if (!rule.use || !Array.isArray(rule.use) || !rule.test) { - return - } + if (!rule.use || !Array.isArray(rule.use) || !rule.test) + return rule + + const test = rule.test as RegExp + const isTypescript = test.test('test.ts') + const isJavascript = test.test('test.js') + + if (!isJavascript && !isTypescript) + return rule + // @ts-ignore - cacheLoader = config.module.rules[ruleKey].use.filter((use) => { - return use.loader.includes('cache-loader') - }) - if (env.isDev && rule.test.toString() === '/\\.m?jsx?$/i') { - // Need to strip the thread-loader but keep the cache loader - // @ts-ignore - config.module.rules[ruleKey].use = [ - ...cacheLoader, - { - loader: 'esbuild-loader', - options: { - ...esbuildLoaderOptions - } - } - ] - } else if (rule.test.toString() === '/\\.ts$/i') { - // @ts-ignore - config.module.rules[ruleKey].use = [ - ...cacheLoader, - { - loader: 'esbuild-loader', - options: { - loader: 'ts', - ...esbuildLoaderOptions - } - } - ] - } else if (rule.test.toString() === '/\\.tsx$/i') { - // @ts-ignore - config.module.rules[ruleKey].use = [ - ...cacheLoader, - { - loader: 'esbuild-loader', - options: { - loader: 'ts', - ...esbuildLoaderOptions - } - } - ] + const babelLoaderIndex = rule.use.findIndex((use: RuleSetUseItem) => use.loader.includes('babel-loader')) + if (babelLoaderIndex === -1) + return rule + + const esbuildLoader = { + loader: 'esbuild-loader', + options: { + ...esbuildLoaderOptions, + }, + } + + // in dev we swap out babel for js + if (env.isDev && isJavascript) { + rule.use.splice(babelLoaderIndex, 1, esbuildLoader) + logger.debug(`JS compilation: swapped out babel-loader at index ${babelLoaderIndex} for esbuild`) + return rule } + + // always swap out typescript builds + esbuildLoader.options.loader = 'ts' + rule.use.splice(babelLoaderIndex, 1, esbuildLoader) + // @ts-ignore + const tsLoaderIndex = rule.use.findIndex((use: RuleSetUseItem) => use.loader.includes('ts-loader')) + rule.use.splice(tsLoaderIndex, 1) + logger.debug(`TS compilation: swapped out ts-loader at index ${tsLoaderIndex} for esbuild`) + return rule }) } - if (options.features.esbuildMinifier && !env.isDev && options.profile !== 'safe' && nuxtOptions.build.optimization) { + // use esbuild to minify js + if (options.features.esbuildMinifier && !env.isDev && options.profile !== RISK_PROFILE_SAFE && nuxtOptions.build.optimization) { const esbuildMinifyOptions = typeof options.esbuildMinifyOptions === 'function' ? options.esbuildMinifyOptions(args) : options.esbuildMinifyOptions // enable esbuild minifier, replace terser nuxtOptions.build.optimization.minimize = true nuxtOptions.build.optimization.minimizer = [ - new ESBuildMinifyPlugin(esbuildMinifyOptions) + new ESBuildMinifyPlugin(esbuildMinifyOptions), ] // make sure terser is off nuxtOptions.build.terser = false + logger.debug('JS Minify: swapped out terser for esbuild minify') } } diff --git a/src/optimisations/images.ts b/src/optimisations/images.ts index 165f5d2..db7f1db 100644 --- a/src/optimisations/images.ts +++ b/src/optimisations/images.ts @@ -1,24 +1,23 @@ import type { RuleSetRule } from 'webpack' import type { OptimisationArgs } from '../types' -export default ({ config, env, options } : OptimisationArgs) => { - if (!config.module || !env.isDev || !options.features.imageFileLoader) { +export default ({ config, env, options, logger }: OptimisationArgs) => { + if (!config.module || !env.isDev || !options.features.imageFileLoader) return - } const imgLoaders = config.module.rules.filter(r => // make sure there is a test available - r.test && + r.test // we don't want to match resource queries such as ?inline, it's possible this is nested within oneOf though - !r.resourceQuery && + && !r.resourceQuery // only basic image formats, we don't want to match svg in case there's a specific svg-loader // @ts-ignore - ('.png'.match(r.test) || '.jpg'.match(r.test)) + && ('.png'.match(r.test) || '.jpg'.match(r.test)), ) // if for some reason there is no png loader - if (!imgLoaders.length) { + if (!imgLoaders.length) return - } + // only match the first rule we find const firstImgLoader = imgLoaders[0] as RuleSetRule // remove the current image loader for pngs @@ -35,9 +34,11 @@ export default ({ config, env, options } : OptimisationArgs) => { loader: 'file-loader', options: { name: '[path][name].[ext]', - esModule: false - } - } - ] + esModule: false, + }, + }, + ], }) + + logger.debug('Image loader: Swapped out url-loader with file loader') } diff --git a/src/optimisations/nuxt.ts b/src/optimisations/nuxt.ts index 20e1a91..5c6806d 100644 --- a/src/optimisations/nuxt.ts +++ b/src/optimisations/nuxt.ts @@ -1,44 +1,48 @@ import { existsSync } from 'fs' +import { cpus } from 'os' import { join } from 'upath' import type { OptimisationArgs } from '../types' +import { RISK_PROFILE_SAFE, RISK_PROFILE_RISKY } from '../constants' +import logger from '../logger' -export default ({ options, nuxtOptions, env } : OptimisationArgs) => { - if (options.profile !== 'safe' && options.features.cacheLoader) { +export default ({ options, nuxtOptions, env }: OptimisationArgs) => { + if (options.profile !== RISK_PROFILE_SAFE && options.features.cacheLoader) nuxtOptions.build.cache = true - } - if (options.profile === 'risky') { + + if (options.profile === RISK_PROFILE_RISKY) { if (!options.measure) { - if (options.features.hardSourcePlugin) { + if (options.features.hardSourcePlugin) nuxtOptions.build.hardSource = true + + if (options.features.parallelPlugin) { + const cpuCount = cpus().length + // check it's worth turning on + if (cpuCount > 1) + nuxtOptions.build.parallel = true + else + logger.warn('Not enabling parallel loader due to limited CPU capacity. Consider disabling `parallelPlugin`.') } - const os = require('os') - const cpuCount = os.cpus().length - // check it's worth turning on - if (cpuCount > 1) { - nuxtOptions.build.parallel = true - } else { - console.info('Not enabling parallel loader due to limited CPU capacity.') - } - } else { - console.info('Parallel loader and hardsource optimisations disabled while `measure` is enabled.') + } + else { + logger.warn('Parallel loader and hardsource optimisations disabled while `measure` is enabled.') } } if (env.isDev) { // disable modern since the client build will be modern already nuxtOptions.modern = false + logger.debug('Nuxt: Disabled modern mode') // disable js minification in dev nuxtOptions.build.terser = false - if (nuxtOptions.build.html) { - // @ts-ignore + if (nuxtOptions.build.html) nuxtOptions.build.html.minify = false - } + // set the postcss stage to false to avoid pollyfills - if (options.features.postcssNoPolyfills && - options.profile !== 'safe' && + if (options.features.postcssNoPolyfills + && options.profile !== RISK_PROFILE_SAFE // make sure we have postcss and a preset set - nuxtOptions.build.postcss && + && nuxtOptions.build.postcss // @ts-ignore - nuxtOptions.build.postcss.preset + && nuxtOptions.build.postcss.preset ) { // @ts-ignore nuxtOptions.build.postcss.preset.stage = false @@ -47,10 +51,10 @@ export default ({ options, nuxtOptions, env } : OptimisationArgs) => { // disable features not used // little bit risky because modules could be doing something weird - if (options.profile !== 'safe') { + if (options.profile !== RISK_PROFILE_SAFE) { const folderFeatures = [ 'layouts', - 'store' + 'store', ] folderFeatures.forEach((f: string) => { // @ts-ignore diff --git a/src/optimisations/webpack.ts b/src/optimisations/webpack.ts index dbb042a..a66797d 100644 --- a/src/optimisations/webpack.ts +++ b/src/optimisations/webpack.ts @@ -1,9 +1,9 @@ import { OptimisationArgs } from '../types' -export default ({ config, env, options } : OptimisationArgs) => { - if (!config.resolve || !config.output || !config.optimization || !options.features.webpackOptimisations) { +export default ({ config, env, options }: OptimisationArgs) => { + if (!config.resolve || !config.output || !config.optimization || !options.features.webpackOptimisations) return - } + /* Webpack Optimisations: https://webpack.js.org/guides/build-performance/ */ config.output.pathinfo = false config.output.futureEmitAssets = true diff --git a/src/tools/speed-measure-plugin.ts b/src/tools/speed-measure-plugin.ts index 34c63f3..9a45cd0 100644 --- a/src/tools/speed-measure-plugin.ts +++ b/src/tools/speed-measure-plugin.ts @@ -2,46 +2,46 @@ import { existsSync } from 'fs' import { resolve } from 'upath' import { sync as rimrafSync } from 'rimraf' import SpeedMeasurePlugin from 'speed-measure-webpack-plugin' -import type { Configuration as WebpackConfig } from 'webpack' +import { Configuration as WebpackConfiguration } from 'webpack' import type { OptimisationArgs } from '../types' - +import logger from '../logger' /* Speed Measure Plugin: https://www.npmjs.com/package/speed-measure-webpack-plugin */ -export default ({ options } : OptimisationArgs, nuxt : any) => { - if (!options.measure) { +export default ({ options }: OptimisationArgs, nuxt: any) => { + if (!options.measure) return - } + // breaks if SSR is off for some reason if (!nuxt.options.ssr) { options.measure = false - console.warn('SpeedMeasurePlugin has not been enabled because SSR mode is off.') + logger.warn('SpeedMeasurePlugin has not been enabled because SSR mode is off.') return } // running in test mode does not seem like a good idea if (process.env.NODE_ENV === 'test') { options.measure = false - console.warn('SpeedMeasurePlugin has been disabled because of the testing environment.') + logger.warn('SpeedMeasurePlugin has been disabled because of the testing environment.') return } // remove the .cache folder to resolve any weird issues around caching if (options.profile !== 'safe' && process.env.INIT_CWD && (options.features.cacheLoader || options.features.hardSourcePlugin)) { const cacheFolder = resolve('./node_modules/.cache') - if (existsSync(cacheFolder)) { + if (existsSync(cacheFolder)) rimrafSync(cacheFolder) - } } const defaults = { - outputFormat: 'human' + outputFormat: 'human', } const measureOptions = { ...defaults, - ...(typeof options.measure === 'boolean' ? {} : options.measure) + ...(typeof options.measure === 'boolean' ? {} : options.measure), } as SpeedMeasurePlugin.Options const smp = new SpeedMeasurePlugin(measureOptions) - nuxt.hook('webpack:config', (configs: WebpackConfig[]) => { + nuxt.hook('webpack:config', (configs: WebpackConfiguration[]) => { configs.forEach((config) => { if (config.name === options.measureMode || options.measureMode === 'all') { + // @ts-ignore smp.wrap(config) - console.info(`SpeedMeasurePlugin is enabled for \`${config.name}\`. Build time may be effected.`) + logger.info(`SpeedMeasurePlugin is enabled for \`${config.name}\`. Build time may be effected.`) } }) }) diff --git a/src/types.ts b/src/types.ts index be689fd..74405e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,10 @@ import type { Configuration as WebpackConfig } from 'webpack' import { ExtendFunctionContext } from '@nuxt/types/config/module' import type { NuxtOptions } from '@nuxt/types' import type { LoaderOptions, MinifyPluginOptions } from 'esbuild-loader/dist/interfaces' +import { Consola } from 'consola' + +export type RiskProfile = 'risky' | 'experimental' | 'safe' +export type MeasureMode = 'client' | 'server' | 'modern' | 'all' export interface FeatureFlags { // uses esbuild loader @@ -19,6 +23,8 @@ export interface FeatureFlags { cacheLoader: boolean // use the hardsource plugin hardSourcePlugin: boolean + // use the parallel thread plugin + parallelPlugin: boolean } export interface OptimisationArgs { @@ -26,13 +32,14 @@ export interface OptimisationArgs { // eslint-disable-next-line no-use-before-define options: ModuleOptions config: WebpackConfig + logger: Consola env: ExtendFunctionContext } export interface ModuleOptions { - measureMode: 'client' | 'server' | 'modern' | 'all' + measureMode: MeasureMode measure: boolean | SpeedMeasurePlugin.Options - profile: 'risky' | 'experimental' | 'safe' | false + profile: RiskProfile | false esbuildLoaderOptions: LoaderOptions | ((args: OptimisationArgs) => LoaderOptions) esbuildMinifyOptions: MinifyPluginOptions | ((args: OptimisationArgs) => MinifyPluginOptions) features: FeatureFlags