@plone/volto
Version:
Volto
466 lines (426 loc) • 16.3 kB
JavaScript
/* eslint no-console: 0 */
const path = require('path');
const makeLoaderFinder = require('razzle-dev-utils/makeLoaderFinder');
const nodeExternals = require('webpack-node-externals');
const LoadablePlugin = require('@loadable/webpack-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const fs = require('fs');
const RootResolverPlugin = require('./webpack-plugins/webpack-root-resolver');
const RelativeResolverPlugin = require('./webpack-plugins/webpack-relative-resolver');
const { poToJson } = require('@plone/scripts/i18n.cjs');
const { createAddonsLoader } = require('@plone/registry/create-addons-loader');
const {
createThemeAddonsLoader,
} = require('@plone/registry/create-theme-loader');
const { AddonRegistry } = require('@plone/registry/addon-registry');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
const AfterBuildPlugin = require('@fiverr/afterbuild-webpack-plugin');
const fileLoaderFinder = makeLoaderFinder('file-loader');
const projectRootPath = path.resolve('.');
const languages = require('./src/constants/Languages.cjs');
const packageJson = require(path.join(projectRootPath, 'package.json'));
const { registry } = AddonRegistry.init(projectRootPath);
const defaultModify = ({
env: { target, dev },
webpackConfig: config,
webpackObject: webpack,
options,
paths,
}) => {
// Compile language JSON files from po files
poToJson({ registry, addonMode: false });
if (dev) {
config.plugins.unshift(
new webpack.DefinePlugin({
__DEVELOPMENT__: true,
}),
);
} else {
config.plugins.unshift(
new webpack.DefinePlugin({
__DEVELOPMENT__: false,
}),
);
}
if (target === 'web') {
config.plugins.unshift(
new webpack.DefinePlugin({
__CLIENT__: true,
__SERVER__: false,
}),
);
config.plugins.push(
new LoadablePlugin({
outputAsset: false,
writeToDisk: { filename: path.resolve(`${projectRootPath}/build`) },
}),
);
if (dev && process.env.DEBUG_CIRCULAR) {
config.plugins.push(
new CircularDependencyPlugin({
exclude: /node_modules/,
// `onStart` is called before the cycle detection starts
onStart({ compilation }) {
console.log('start detecting webpack modules cycles');
},
failOnError: false,
// `onDetected` is called for each module that is cyclical
onDetected({ module: webpackModuleRecord, paths, compilation }) {
// `paths` will be an Array of the relative module paths that make up the cycle
// `module` will be the module record generated by webpack that caused the cycle
compilation.warnings.push(new Error(paths.join(' -> ')));
},
// `onEnd` is called before the cycle detection ends
onEnd({ compilation }) {
console.log(
`Detected ${compilation.warnings.length} circular dependencies`,
);
compilation.warnings.forEach((item) => {
if (item.message.includes('config')) {
console.log(item.message);
}
});
},
}),
);
}
config.output.filename = dev
? 'static/js/[name].js'
: 'static/js/[name].[chunkhash:8].js';
config.optimization = Object.assign({}, config.optimization, {
runtimeChunk: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
// We reset the default values set by webpack
// So the chunks have all proper names (no random numbers)
// The CSS gets bundled in one CSS chunk and it's consistent with
// the `style-loader` load order, so no difference between
// local (project CSS) and `node_modules` ones.
default: false,
defaultVendors: false,
},
},
});
// This is needed to override Razzle use of the unmaintained CleanCSS
// which does not have support for recently CSS features (container queries).
// Using the default provided (cssnano) by css-minimizer-webpack-plugin
// should be enough see:
// (https://github.com/clean-css/clean-css/discussions/1209)
delete options.webpackOptions.terserPluginOptions?.sourceMap;
if (!dev) {
config.optimization = Object.assign({}, config.optimization, {
minimizer: [
new TerserPlugin(options.webpackOptions.terserPluginOptions),
new CssMinimizerPlugin({
sourceMap: options.razzleOptions.enableSourceMaps,
minimizerOptions: {
sourceMap: options.razzleOptions.enableSourceMaps,
},
}),
],
});
}
config.plugins.unshift(
// restrict moment.js locales to supported languages
// see https://momentjs.com/docs/#/use-it/webpack/ for details
new MomentLocalesPlugin({ localesToKeep: Object.keys(languages) }),
new LodashModuleReplacementPlugin({
shorthands: true,
cloning: true,
currying: true,
caching: true,
collections: true,
exotics: true,
guards: true,
metadata: true,
deburring: true,
unicode: true,
chaining: true,
memoizing: true,
coercions: true,
flattening: true,
paths: true,
placeholders: true,
}),
);
// This copies the publicPath files set in voltoConfigJS with the local `public`
// directory at build time
config.plugins.push(
new AfterBuildPlugin(() => {
const mergeDirectories = (sourceDir, targetDir) => {
const files = fs.readdirSync(sourceDir);
files.forEach((file) => {
const sourcePath = path.join(sourceDir, file);
const targetPath = path.join(targetDir, file);
const isDirectory = fs.statSync(sourcePath).isDirectory();
if (isDirectory) {
fs.mkdirSync(targetPath, { recursive: true });
mergeDirectories(sourcePath, targetPath);
} else {
fs.copyFileSync(sourcePath, targetPath);
}
});
};
// If we are in development mode, we copy the public directory to the
// public directory of the setup root, so the files are available
if (dev && !registry.isVoltoProject && registry.addonNames.length > 0) {
const devPublicPath = `${projectRootPath}/../../../public`;
if (!fs.existsSync(devPublicPath)) {
fs.mkdirSync(devPublicPath);
}
mergeDirectories(
path.join(projectRootPath, 'public'),
`${projectRootPath}/../../../public`,
);
}
registry.getAddonDependencies().forEach((addonDep) => {
// What comes from getAddonDependencies is in the form of `@package/addon:profile`
const addon = addonDep.split(':')[0];
// Check if the addon is available in the registry, just in case
if (registry.packages[addon]) {
const p = fs.realpathSync(
`${registry.packages[addon].modulePath}/../.`,
);
if (fs.existsSync(path.join(p, 'public'))) {
if (!dev && fs.existsSync(paths.appBuildPublic)) {
mergeDirectories(path.join(p, 'public'), paths.appBuildPublic);
}
if (
dev &&
!registry.isVoltoProject &&
registry.addonNames.length > 0
) {
mergeDirectories(
path.join(p, 'public'),
`${projectRootPath}/../../../public`,
);
}
}
}
});
}),
);
}
if (target === 'node') {
config.plugins.unshift(
new webpack.DefinePlugin({
__CLIENT__: false,
__SERVER__: true,
}),
);
// Make the TerserPlugin accept ESNext features, since we are in 2024
// If this is not true, libraries already compiled for using only ESNext features
// won't work (eg. using a chaining operator)
config.optimization = Object.assign({}, config.optimization, {
minimizer: [
new TerserPlugin({
terserOptions: {
parse: { ecma: 'ESNext' },
},
}),
],
});
// Razzle sets some of its basic env vars in the default config injecting them (for
// the client use, mainly) in a `DefinePlugin` instance. However, this also ends in
// the server build, removing the ability of the server node process to read from
// the system's (or process') env vars. In this case, in the server build, we hunt
// down the instance of the `DefinePlugin` defined by Razzle and remove the
// `process.env.PORT` so it can be overridable in runtime
const idxArr = config.plugins
.map((plugin, idx) =>
plugin.constructor.name === 'DefinePlugin' ? idx : '',
)
.filter(String);
idxArr.forEach((index) => {
const { definitions } = config.plugins[index];
if (definitions['process.env.PORT']) {
const newDefs = Object.assign({}, definitions);
// Transforms the stock RAZZLE_PUBLIC_DIR into relative path,
// so one can move the build around
newDefs['process.env.RAZZLE_PUBLIC_DIR'] = newDefs[
'process.env.RAZZLE_PUBLIC_DIR'
].replace(projectRootPath, '.');
// Handles the PORT, so it takes the real PORT from the runtime environment var,
// but keeps the one from build time, if different from 3000 (by not deleting it)
// So build time one takes precedence, do not set it in build time if you want
// to control it always via runtime (assuming 3000 === not set in build time)
if (newDefs['process.env.PORT'] === '3000') {
delete newDefs['process.env.PORT'];
}
config.plugins[index] = new webpack.DefinePlugin(newDefs);
}
});
}
// Don't load config|variables|overrides) files with file-loader
// Don't load SVGs from ./src/icons with file-loader
const fileLoader = config.module.rules.find(fileLoaderFinder);
fileLoader.exclude = [
/\.(config|variables|overrides|cjs)$/,
/icons\/.*\.svg$/,
...fileLoader.exclude,
];
let addonsFromEnvVar = [];
if (process.env.ADDONS) {
addonsFromEnvVar = process.env.ADDONS.split(';');
}
const addonsLoaderPath = createAddonsLoader(
registry.getAddonDependencies(),
registry.getAddons(),
// The load of the project config is deprecated and will be removed in Volto 19.
{ loadProjectConfig: true },
);
config.resolve.plugins = [
new RelativeResolverPlugin(registry),
new RootResolverPlugin(),
];
config.resolve.alias = {
...registry.getAddonCustomizationPaths(),
...registry.getAddonsFromEnvVarCustomizationPaths(),
...registry.getProjectCustomizationPaths(),
...config.resolve.alias,
'../../theme.config$': `${projectRootPath}/theme/theme.config`,
'volto-themes': `${registry.voltoPath}/theme/themes`,
'load-volto-addons': addonsLoaderPath,
...registry.getResolveAliases(),
'@plone/volto': `${registry.voltoPath}/src`,
// to be able to reference path uncustomized by webpack
'@plone/volto-original': `${registry.voltoPath}/src`,
// be able to reference current package from customized package
'@package': `${projectRootPath}/src`,
'@root': `${projectRootPath}/src`,
// we're incorporating redux-connect
'redux-connect': `${registry.voltoPath}/src/helpers/AsyncConnect`,
// avoids including lodash multiple times.
// semantic-ui-react uses lodash-es, everything else uses lodash
'lodash-es': path.dirname(require.resolve('lodash')),
};
const [addonsThemeLoaderVariablesPath, addonsThemeLoaderMainPath] =
createThemeAddonsLoader(registry.getCustomThemeAddons());
// Automatic Theme Loading
if (registry.theme) {
// The themes should be located in `src/theme`
const themePath = registry.packages[registry.theme].modulePath;
const themeConfigPath = `${themePath}/theme/theme.config`;
config.resolve.alias['../../theme.config$'] = themeConfigPath;
config.resolve.alias['../../theme.config'] = themeConfigPath;
// We create an alias for each custom theme insertion point (variables, main)
config.resolve.alias['addonsThemeCustomizationsVariables'] =
addonsThemeLoaderVariablesPath;
config.resolve.alias['addonsThemeCustomizationsMain'] =
addonsThemeLoaderMainPath;
}
config.performance = {
maxAssetSize: 10000000,
maxEntrypointSize: 10000000,
};
let addonsAsExternals = [];
const { include } = options.webpackOptions.babelRule;
if (packageJson.name !== '@plone/volto') {
include.push(fs.realpathSync(`${registry.voltoPath}/src`));
}
// Add babel support external (ie. node_modules npm published packages)
const packagesNames = Object.keys(registry.packages);
if (registry.packages && packagesNames.length > 0) {
packagesNames.forEach((addon) => {
const p = fs.realpathSync(registry.packages[addon].modulePath);
if (include.indexOf(p) === -1) {
include.push(p);
}
});
addonsAsExternals = packagesNames.map((addon) => new RegExp(addon));
}
if (process.env.ADDONS) {
addonsFromEnvVar.forEach((addon) => {
const normalizedAddonName = addon.split(':')[0];
const p = fs.realpathSync(
registry.packages[normalizedAddonName].modulePath,
);
if (include.indexOf(p) === -1) {
include.push(p);
}
addonsAsExternals = [
...addonsAsExternals,
...packagesNames.map(
(normalizedAddonName) => new RegExp(normalizedAddonName),
),
];
});
}
// write a .dot file with the graph
// convert it to svg with: `dot addon-dependency-graph.dot -Tsvg -o out.svg`
if (process.env.DEBUG_ADDONS_LOADER && target === 'node') {
const addonsDepGraphPath = path.join(
process.cwd(),
'addon-dependency-graph.dot',
);
const graph = registry.getDotDependencyGraph();
fs.writeFileSync(addonsDepGraphPath, new Buffer.from(graph));
}
config.externals =
target === 'node'
? [
nodeExternals({
allowlist: [
dev ? 'webpack/hot/poll?300' : null,
/\.(eot|woff|woff2|ttf|otf)$/,
/\.(svg|png|jpg|jpeg|gif|ico)$/,
/\.(mp4|mp3|ogg|swf|webp)$/,
/\.(css|scss|sass|sss|less)$/,
// Add support for addons to include externals (ie. node_modules npm published packages)
...addonsAsExternals,
/^@plone\/volto/,
/^@plone\/components/,
/^@plone\/client/,
/^@plone\/providers/,
].filter(Boolean),
}),
]
: [];
return config;
};
const addonExtenders = registry.getAddonExtenders().map((m) => require(m));
const defaultPlugins = [
{ object: require('./webpack-plugins/webpack-less-plugin')({ registry }) },
{ object: require('./webpack-plugins/webpack-svg-plugin') },
{ object: require('./webpack-plugins/webpack-bundle-analyze-plugin') },
{ object: require('./jest-extender-plugin') },
'scss',
];
const plugins = addonExtenders.reduce(
(acc, extender) => extender.plugins(acc),
defaultPlugins,
);
module.exports = {
plugins,
modifyJestConfig: ({ jestConfig }) => {
jestConfig.testEnvironment = 'jsdom';
return jestConfig;
},
modifyWebpackConfig: ({
env: { target, dev },
webpackConfig,
webpackObject,
options,
paths,
}) => {
const defaultConfig = defaultModify({
env: { target, dev },
webpackConfig,
webpackObject,
options,
paths,
});
const res = addonExtenders.reduce(
(acc, extender) => extender.modify(acc, { target, dev }, webpackConfig),
defaultConfig,
);
return res;
},
options: {
enableReactRefresh: true,
},
};