canvas-lms/ui-build/webpack/webpack.plugins.js

231 lines
8.2 KiB
JavaScript

/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable import/no-extraneous-dependencies */
const {EnvironmentPlugin, DefinePlugin, IgnorePlugin} = require('webpack')
const {sync} = require('glob')
const {join, resolve} = require('path')
const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin')
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')
const EsmacPlugin = require('webpack-esmac-plugin')
const {WebpackManifestPlugin} = require('webpack-manifest-plugin')
const {RetryChunkLoadPlugin} = require('webpack-retry-chunk-load-plugin')
// keep this in sync with webpack's dep version
// uses terser to minify JavaScript
const TerserPlugin = require('terser-webpack-plugin')
const SourceFileExtensionsPlugin = require('./SourceFileExtensionsPlugin')
const WebpackHooks = require('./webpackHooks')
// determines which folder public assets are compiled to
const webpackPublicPath = require('./webpackPublicPath')
const {canvasDir} = require('../params')
// sets these environment variables in compiled code.
// process.env.NODE_ENV will make it so react and others are much smaller and don't run their
// debug/propType checking in prod.
exports.environmentVars = new EnvironmentPlugin({
NODE_ENV: null,
GIT_COMMIT: null,
ALWAYS_APPEND_UI_TESTABLE_LOCATORS: null,
})
// Only include timezone data starting from 2011 (canvaseption) to 15 years from now,
// so we don't clutter the vendor bundle with a bunch of old timezone data
exports.timezoneData = new MomentTimezoneDataPlugin({
startYear: 2011,
endYear: new Date().getFullYear() + 15,
})
// allow plugins to extend source files
// TODO: remove dependency of Canvas plugins on this extension:
// - instructure_misc_plugin
// - multiple_root_accounts
// - migration_tool
exports.customSourceFileExtensions = new SourceFileExtensionsPlugin({
context: canvasDir,
include: sync(join(canvasDir, 'gems/plugins/*/package.json'), {absolute: true}),
tmpDir: join(canvasDir, 'tmp/webpack-source-file-extensions'),
})
// hooks for webpack lifecycle (start, fail, done)
// requires process.env.ENABLE_CANVAS_WEBPACK_HOOKS
// write your own custom commands for:
// CANVAS_WEBPACK_START_HOOK
// CANVAS_WEBPACK_FAILED_HOOK
// CANVAS_WEBPACK_DONE_HOOK
// cf. ui-build/webpack/webpackHooks/macNotifications.sh
exports.webpackHooks = new WebpackHooks()
// controls access between modules; enforces where you can import from
// i.e. can't import features into packages, or ui/shared into packages
exports.controlAccessBetweenModules = new EsmacPlugin({
test: /\.[tj]sx?$/,
include: [
resolve(canvasDir, 'ui'),
resolve(canvasDir, 'packages'),
resolve(canvasDir, 'public/javascripts'),
resolve(canvasDir, 'gems/plugins'),
],
exclude: [/\/node_modules\//],
formatter: require('./esmac/ErrorFormatter'),
rules: require('./esmac/moduleAccessRules'),
permit:
process.env.WEBPACK_ENCAPSULATION_DEBUG === '1'
? []
: require('./esmac/errorsPendingRemoval.json'),
})
exports.setMoreEnvVars = new DefinePlugin({
CANVAS_WEBPACK_PUBLIC_PATH: JSON.stringify(webpackPublicPath),
NODE_ENV: null,
// webpack5 stopped providing a polyfill for process.env and its use in
// web code is discouraged but a number of our dependencies still rely on
// this, so we either selectively shim every property that they may be
// referencing through the EnvironmentPlugin (below) and risk a hard
// runtime error in case we didn't cover them all, or provide a sink like
// this, which i'm gonna go with for now:
process: {browser: true, env: {}},
})
// tries to load chunks if they fail to load
// to resolve
exports.retryChunkLoading = new RetryChunkLoadPlugin({
maxRetries: 3,
retryDelay: `function(retryAttempt) { return retryAttempt * 1000 }`,
})
// prevents writing to the cache
exports.readOnlyCache = function (compiler) {
compiler.cache.hooks.store.intercept({
register: tapInfo => ({...tapInfo, fn: () => {}}),
})
}
// return a non-zero exit code if there are any warnings so we don't
// continue compiling assets if webpack fails
exports.failOnWebpackWarnings = function (compiler) {
compiler.hooks.done.tap('Canvas:FailOnWebpackWarnings', compilation => {
if (compilation.warnings && compilation.warnings.length) {
// eslint-disable-next-line no-console
console.error(compilation.warnings)
// If there's a bad import, webpack doesn't say where.
// Only if we let the compilation complete do we get
// the callstack where the import happens
// If you're having problems, comment out the following
throw new Error('webpack build had warnings. Failing.')
}
})
}
exports.analyzeBundles = new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: process.env.WEBPACK_ANALYSIS_FILE
? resolve(process.env.WEBPACK_ANALYSIS_FILE)
: resolve(canvasDir, 'tmp/webpack-bundle-analysis.html'),
openAnalyzer: false,
generateStatsFile: false,
statsOptions: {
source: false,
},
})
// don't include any of the moment locales in the common bundle
// (otherwise it is huge!) we load them explicitly onto the page in
// include_js_bundles from rails.
exports.excludeMomentLocales = new IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
})
// generates asset manifests; maps features with chunks
// outputs a json file so Rails knows which hash fingerprints to add
// to each script url and so it knows which split chunks to make a
// <link rel=preload ... /> for for each `js_bundle`
exports.webpackManifest = new WebpackManifestPlugin({
fileName: 'webpack-manifest.json',
publicPath: '',
useEntryKeys: true,
})
exports.minimizeCode = new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
sequences: false, // prevents it from combining a bunch of statements with ","s so it is easier to set breakpoints
// these are all things that terser does by default but we turn
// them off because they don't reduce file size enough to justify the
// time they take, especially after gzip:
// see: https://slack.engineering/keep-webpack-fast-a-field-guide-for-better-build-performance-f56a5995e8f1
booleans: false,
collapse_vars: false,
comparisons: false,
computed_props: false,
hoist_props: false,
if_return: false,
join_vars: false,
keep_infinity: true,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
typeofs: false,
},
output: {
comments: false,
semicolons: false, // prevents everything being on one line so it's easier to view in devtools
},
},
})
exports.buildCacheOptions = {
cache: {
type: 'filesystem',
allowCollectingMemory: false,
buildDependencies: {config: []},
compression: 'gzip',
},
snapshot: {
buildDependencies: {hash: true, timestamp: false},
module: {hash: true, timestamp: false},
resolve: {hash: true, timestamp: false},
resolveBuildDependencies: {hash: true, timestamp: false},
},
}
// style of source mapping to enhance the debugging process
// https://webpack.js.org/configuration/devtool/
exports.getDevtool = function (skipSourcemaps) {
let devtool
if (skipSourcemaps) {
// Fast
devtool = false
} else if (process.env.NODE_ENV === 'production' || process.env.COVERAGE === '1') {
// Slow. "Recommended choice for production builds with high quality SourceMaps.""
devtool = 'source-map'
} else {
// "Recommended choice for development builds with maximum performance"
devtool = 'eval'
}
return devtool
}