Files
kupshop/web/common/webpack/webpack.config.ts
2025-08-02 16:30:27 +02:00

444 lines
15 KiB
TypeScript

import Encore from '@symfony/webpack-encore';
console.log(`Webpack configuration - development: ${Encore.isDev()}, production: ${Encore.isProduction()}, devServer: ${Encore.isDevServer()}`);
import WpjResolver from './WpjResolver';
import { IComponents } from './WpjConfig';
const path = require('path');
const fs = require('fs');
const process = require('process');
const handlebars = require('handlebars');
const CompressionPlugin = require('compression-webpack-plugin');
const zlib = require('zlib')
const WpjComponentLoader = require('./WpjComponentLoader');
const sass = require("sass");
const { fullConfig, components, modulesWithValues, modules , hasComponents, entrypoints} = require('./WpjConfig');
const sassUtils = require('node-sass-utils')(sass);
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
WpjComponentLoader.setComponents(fullConfig.components);
const module_template = handlebars.compile(fs.readFileSync(__dirname + '/scss_modules.hbs', 'utf8'));
const scss_modules = module_template(fullConfig);
const rootDir = process.cwd();
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
// token for uploading webpack sourcemaps into sentry
const sentryUploadToken = Encore.isProduction() && hasComponents && (process.env.SENTRY_UPLOAD_TOKEN || false);
Encore
// directory where compiled assets will be stored
.setOutputPath('web/build/')
// public path used by the web server to access the output path
.setPublicPath('/web/build')
// only needed for CDN's or sub-directory deploy
//.setManifestKeyPrefix('build/')
// Every entrypoint here is NEEDED in BOTH component (for fallback) a non-component shops
// Otherwise move entrypoint to list downward
.addEntry('icons', '@assets/entrypoints/icons.js')
.addEntry('app', '@assets/entrypoints/app.js')
.addEntry('htmlpage', '@assets/entrypoints/htmlpage.js') // TODO: předělat 404, pak může dolů
.addEntry('cart', '@assets/entrypoints/cart.js')
.addEntry('shopping_list', '@assets/entrypoints/shopping-list.js') // TODO: předělat nákupní seznamy, pak může dolů
.addEntry('wpj_toolbar', '@assets/entrypoints/wpj_toolbar.js') // TODO: předělat WpjToolbar, pak může dolů
.addEntry('user', '@assets/entrypoints/user.js') // TODO: předělat, pak může dolů
//.addEntry('page2', './assets/js/page2.js')
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()
.splitEntryChunks()
.enableSourceMaps(!Encore.isProduction() || !!sentryUploadToken)
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// uncomment if you use Sass/SCSS files
.enableSassLoader(options => {
// https://github.com/sass/node-sass#options
options.sassOptions.includePaths = [path.join(rootDir, 'engine/web/common/static/compass/'), path.join(rootDir, 'common')];
options.implementation = require('sass');
options.sassOptions.data = (loaderContext) => {
// More information about available properties https://webpack.js.org/api/loaders/
const { resourcePath, rootContext } = loaderContext;
return scss_modules;
};
options.sassOptions.functions = {
"component($component, $value)": function(name, value) {
name = name.getValue();
value = value.getValue();
const component = fullConfig.components[name] ?? {class: 'c-unknown-component'};
let result = component[value];
if (value === 'class') {
result = 'c-' + result;
}
return sassUtils.castToSass(result);
}
}
})
.enablePostCssLoader(options => {
options.postcssOptions = {
// the directory where the postcss.config.js file is stored
config: path.join(rootDir, 'engine/web/common/webpack/')
};
})
.configureDefinePlugin(options => {
options.ADMIN = false;
options.MODULES = modules;
options.COMPONENTS = Object.fromEntries(Object.entries(components as IComponents).map(([name, component]) => {
return [name, {version: component.version, selector: component.selector}];
}));
})
.configureDevServerOptions(options => {
options.server = {
type: 'https',
options: {
cert: '/etc/ssl/cert.pem',
key: '/etc/ssl/key.pem',
},
};
options.hot = true;
options.allowedHosts = 'all';
options.host = '0.0.0.0';
options.client = {
overlay: true,
progress: true,
};
options.proxy = {
secure: false,
context: () => true,
target: 'https://localhost:443'
};
})
.addAliases({
'@webpack': path.join(rootDir, 'engine/web/common/webpack/assets/'),
})
if (hasComponents) {
Encore
.addEntry('base', '@assets/components/base.ts')
.addEntry('sentry', '@assets/entrypoints/sentry.js');
for (const entrypoint of Array.from(entrypoints.keys())) {
Encore.addEntry(`c-${entrypoint}`, `WpjComponentLoader.ts!?entrypoint=${entrypoint}`);
}
const CopyWebpackPlugin = require('copy-webpack-plugin');
Encore.addPlugin(
new CopyWebpackPlugin({
patterns: [
{ from: path.resolve(rootDir, 'engine/web/common/webpack/assets/icons'), to: 'icons', priority: 5 },
{ from: 'assets/icons', to: 'icons', force: true, priority: 10 }
],
})
)
Encore.addPlugin(
new CopyWebpackPlugin({
patterns: [
{ from: path.resolve(rootDir, 'engine/web/common/webpack/assets/images'), to: 'images/[name].[hash][ext]', priority: 5 },
{ from: 'assets/images', to: 'images/[name].[hash][ext]', force: true, priority: 10 }
],
})
)
} else {
Encore
.addEntry('product', '@assets/entrypoints/product.js')
.addEntry('category', '@assets/entrypoints/category.js')
.addEntry('home', '@assets/entrypoints/home.js')
.addEntry('articles', '@assets/entrypoints/articles.js')
.addEntry('last_visited_products', '@assets/entrypoints/last_visited_products.js')
}
if (Encore.isDevServer()) {
Encore.disableCssExtraction();
}
Encore.cleanupOutputBeforeBuild(['**/*'], (options) => {
options.cleanOnceBeforeBuildPatterns.push(...[
'!icons/**',
'!icons-preview.*',
'!app.icons.*',
]);
// options.verbose = true;
})
if (!Encore.isProduction()) {
Encore.enableBuildCache({
// object of "buildDependencies"
// https://webpack.js.org/configuration/other-options/#cachebuilddependencies
// __filename means that changes to webpack.config.ts should invalidate the cache
config: [__filename],
});
}
/* Při lokálním buildu přes prepareWeb chceme pokaždé unikátní cestu k souboru kvůli cache busting */
if (Encore.isDev() && !Encore.isDevServer()) {
Encore.configureFilenames({
'js': '[name].js?id=[chunkhash]',
'css': '[name].css?id=[chunkhash]'
});
}
if (Encore.isProduction()) {
Encore.addPlugin(
new CompressionPlugin({
algorithm: 'gzip',
})
).addPlugin(
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
compressionOptions: {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
},
},
})
)
}
if (modules['PRODUCTS_COMPARE'] === 'true') {
Encore.addEntry('product-compare', '@assets/entrypoints/product-compare.js');
}
if (modules['RETURNS'] === 'true' || modules['RECLAMATIONS'] === 'true') {
Encore.addEntry('returns', '@assets/entrypoints/returns.js');
}
if (modules['B2B_PREORDERS'] === 'true') {
Encore.addEntry('preorders', '@assets/entrypoints/preorders.js');
}
let enableTypeScript = true;
let enableReact = false;
if (modules['JS_SHOP'] === 'true' && !hasComponents) {
Encore.addEntry('js-shop', '@assets/entrypoints/js-shop.js');
enableTypeScript = true;
enableReact = true;
}
if (modules['JS_MENU'] === 'true' && !hasComponents) {
Encore.addEntry('js-menu', '@assets/entrypoints/js-menu.js');
enableTypeScript = true;
enableReact = true;
}
if (modulesWithValues['PRODUCTS__SETS'] === 'js_multisets' && !hasComponents) {
Encore.addEntry('js-multisets', '@assets/entrypoints/js-multisets.js');
enableTypeScript = true;
enableReact = true;
}
if (hasComponents) {
enableTypeScript = true;
enableReact = true;
Encore.addRule({
resourceQuery: /raw/,
type: 'asset/source',
})
.enableStimulusBridge(path.resolve(rootDir, 'engine/web/common/webpack/assets/components/controllers.json'));
}
if (enableTypeScript) {
Encore.enableTypeScriptLoader(options => {
options.transpileOnly = false;
options.onlyCompileBundledFiles = true;
// engine/tsconfig.json
options.configFile = path.resolve(__dirname, '../../../', 'tsconfig.json');
})
}
if(sentryUploadToken) {
Encore.addPlugin(sentryWebpackPlugin({
authToken: sentryUploadToken,
org: 'wpj',
project: 'wpjshop',
sourcemaps: {
filesToDeleteAfterUpload: "web/build/**/*.map*", // clean source maps from prod build after they are uploaded to sentry
},
url: 'https://sentry.wpj.cz/"'
})
)
}
if (enableReact) {
Encore.enableReactPreset();
}
// .configureLoaderRule('eslint', loaderRule => {
// loaderRule.test = /\.(jsx?|vue)$/
// });
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
const EncoreProxy = new Proxy(Encore, {
get: (target, prop) => {
if (prop === 'getWebpackConfig') {
return (...parameters) => {
const config = target[prop](...parameters);
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const loaders = [];
if (!Encore.isDevServer()) {
loaders.push(MiniCssExtractPlugin.loader);
} else {
loaders.push('style-loader');
}
// config.stats = 'verbose';
config.resolve.modules = [path.resolve(rootDir, 'engine/node_modules'), path.resolve(rootDir, 'node_modules'), path.resolve(rootDir)];
config.resolveLoader = config.resolve;
config.module.rules.push({
test: /\.font\.js/,
use: [
...loaders,
{
loader: 'css-loader',
},
{
loader: 'webfonts-loader',
options: {
publicPath: '/web/build/'
}
}
]
});
const WpjResolver = require('./WpjResolver.js');
function getBundlesPaths(lastPath) {
let paths = {};
let index = 0;
for (const bundle in fullConfig['frontend_bundles']) {
let resourcePath = path.join(fullConfig['frontend_bundles'][bundle], lastPath);
if (fs.existsSync(resourcePath)) {
paths['bundle' + (index ? index.toString() : '')] = resourcePath;
++index;
}
}
return paths;
}
config.resolve.plugins = [
new WpjResolver({
rule: /^@assets/,
paths: {
shop: path.join(rootDir, '/assets/'),
...getBundlesPaths('assets/'),
shared: (modules['COMPONENTS'] === 'true') ? path.join(rootDir, 'engine/web/templates/x/static/') : path.join(rootDir, '/static/'),
common: path.join(rootDir, '/engine/web/common/webpack/assets/')
}
})
];
// Zatím nemůžu tohle zakázat, protože jsou tu fallback views a fallback entrypointy, který potřebujou tyhle resolvery
if (true /*!hasComponents*/) {
config.resolve.plugins.push(
new WpjResolver({
rule: /^@css/,
scss: true,
paths: {
shop: path.join(rootDir, '/templates/css/'),
...(hasComponents ? {twig_compat: path.join(rootDir, '/engine/web/templates/twig_compat/scss/')} : {}),
...getBundlesPaths('scss/'),
shared: hasComponents ? path.join(rootDir, 'engine/web/templates/x/static/scss/') : path.join(rootDir, '/static/scss/'),
common: path.join(rootDir, '/engine/web/common/static/scss/')
}
}),
new WpjResolver({
rule: /^@static/,
paths: {
shop: path.join(rootDir, '/assets/'),
...getBundlesPaths('/'),
shared: path.join(rootDir, '/static/'),
common: path.join(rootDir, '/engine/web/common/static/')
}
}),
new WpjResolver({
rule: /^@shop/,
paths: {
shop: rootDir
}
}),
);
}
if (hasComponents) {
config.resolve.plugins.push(new WpjResolver({
rule: /^@twig/,
paths: {
shop: path.join(rootDir, '/twig/'),
...getBundlesPaths('twig/'),
shared: path.join(rootDir, '/engine/web/common/twig/'),
}
}));
for (const [key, bundlePath] of Object.entries(fullConfig['twig_bundles'])) {
config.resolve.plugins.push(new WpjResolver({
rule: new RegExp(`^@${key.replace("Bundle", '')}`),
paths: {
shop: path.join(rootDir, `twig/bundles/${key}/`),
shared: path.join(rootDir, bundlePath),
}
}));
}
}
config.resolve.cacheWithContext = false;
// config.resolve.symlinks = false;
config.resolveLoader = {
modules: [__dirname, path.resolve(rootDir, 'engine/node_modules'), path.resolve(rootDir, 'node_modules')],
}
config.watchOptions = {
ignored: /node_modules/,
};
if (!Encore.isDevServer()) {
// Exit on first error
config.bail = true;
}
console.log('Config ready, starting build ...');
// const util = require('util');
// console.log(util.inspect(config, false, null, true /* enable colors */));
return config;
};
} else if (prop === 'default') {
return EncoreProxy;
}
return (...parameters) => {
const res = target[prop](...parameters);
return (res === target) ? EncoreProxy : res;
};
}
});
module.exports = EncoreProxy;
export default EncoreProxy;