I use twig loader to process twig files. But it doesn't handle resource links like html loader does.
There is a similar question, but the proposed solutions did not help me. When using html-loader extract-loader and twig-loader together, a problem has arisen, because there may be nesting inside the template, and html-loader receives a twig file that has not yet been processed
Webpack Config:
type WebpackBuildMode = "development" | "production";
interface WebpackBuildPaths {
readonly pages: string;
readonly src: string;
readonly output: string;
readonly svgo: string;
readonly public: string;
}
interface WebpackBuildEnv {
readonly mode: WebpackBuildMode,
readonly port: number;
readonly svg: boolean;
}
interface WebpackBuildOptions {
readonly mode: WebpackBuildMode;
readonly paths: WebpackBuildPaths;
readonly isDev: boolean;
readonly port: number;
readonly isSvg: boolean;
}
const buildEntry = (options: WebpackBuildOptions): webpack.Configuration["entry"] => {
const {paths} = options;
const entry = fs.readdirSync(paths.pages).reduce((acc, dir) => {
acc.push([
dir,
path.resolve(paths.pages, dir, 'script.ts')
]);
return acc;
}, []);
return Object.fromEntries(entry);
};
const buildPlugins = (options: WebpackBuildOptions): webpack.Configuration["plugins"] => {
const {paths, isSvg, isDev} = options;
const html = fs.readdirSync(paths.pages).filter(dir => dir !== 'base').map(dir => (
new HtmlWebpackPlugin({
inject: false,
template: path.resolve(paths.pages, dir, "template.twig"),
templateParameters: (compilation, assets, assetTags, options) => {
const compilationAssets = compilation.getAssets();
const sprites = compilationAssets.filter((asset: any) => asset.name.includes('sprites/'));
return {
compilation,
webpackConfig: compilation.options,
htmlWebpackPlugin: {
tags: assetTags,
files: {
...assets,
svgSprites: sprites.map((sprite: any) => ([
sprite.name.split('/')[1],
sprite.source._valueAsString
])),
},
options,
},
};
},
filename: `${dir}.html`,
minify: false,
})
));
const plugins: webpack.Configuration["plugins"] = [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new SvgChunkWebpackPlugin({
filename: "sprites/[name].svg",
generateSpritesPreview: true,
svgstoreConfig: {
svgAttrs: {
'aria-hidden': true,
style: 'position: absolute; width: 0; height: 0; overflow: hidden;'
}
}
}),
];
if (!isSvg) {
plugins.push(...html);
}
return plugins;
};
const buildLoaders = (options: WebpackBuildOptions): webpack.Configuration["module"]["rules"] => {
const {isDev, isSvg, paths} = options;
const tsLoader = {
test: /\.ts$/,
use: "ts-loader",
exclude: /node_modules/,
};
const twigLoader: webpack.RuleSetRule = {
test: /\.twig$/,
use: [
{
loader: "html-loader",
options: {
sources: {
list: [
{
tag: "img",
attribute: "src",
type: "src",
},
],
},
minimize: false,
},
},
{
loader: "twig-loader",
},
],
};
const styleLoader = {
test: /\.s[ac]ss$/i,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"sass-loader",
],
};
const fontLoader = {
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: "asset/resource",
generator: {
filename: 'assets/fonts/[name][ext]'
}
};
const svgLoader = {
test: /\.svg$/i,
include: [
path.resolve(paths.src, "shared", "assets", "icons")
],
use: [
{
loader: (SvgChunkWebpackPlugin as any).loader,
options: {
configFile: paths.svgo,
},
},
],
};
const assetsLoader = {
test: /\.(png|jpg|jpeg|gif|svg)$/i,
type: "asset/resource",
exclude: [
path.resolve(paths.src, "shared", "assets", "icons")
],
generator: {
outputPath: (pathData: any) => {
const dirname = path.dirname(pathData.filename);
let subDir = '';
if (dirname.includes('src/shared/assets/images')) {
subDir = "assets/images/" + dirname.replace('src/shared/assets/images', '') + "/";
}
return subDir;
},
publicPath: (pathData: any) => {
const dirname = path.dirname(pathData.filename);
let subDir = '';
if (dirname.includes('src/shared/assets/images')) {
subDir = "assets/images/" + dirname.replace(/src\/shared\/assets\/images(\/?)/ig, '') + "/";
}
return subDir;
},
filename: '[name][ext]'
}
};
const loaders: webpack.Configuration["module"]["rules"] = [
styleLoader,
fontLoader,
assetsLoader,
svgLoader,
tsLoader,
];
if (!isSvg) {
loaders.push(twigLoader);
}
return loaders;
};
const buildDevServer = (options: WebpackBuildOptions): DevServerConfiguration => {
const {port} = options;
return {
port,
open: true,
hot: 'only',
}
}
const buildResolve = (options: WebpackBuildOptions): webpack.Configuration["resolve"] => {
const {paths} = options;
return {
extensions: [".ts", ".js"]
};
};
const buildWebpackConfig = (options: WebpackBuildOptions): webpack.Configuration => {
const {mode, isDev, isSvg, paths} = options;
return {
mode: mode,
entry: buildEntry(options),
plugins: buildPlugins(options),
module: {
rules: buildLoaders(options),
},
devtool: isDev ? 'inline-source-map' : undefined,
devServer: isDev && !isSvg ? buildDevServer(options) : undefined,
resolve: buildResolve(options),
output: {
path: paths.output,
filename: '[name].js',
clean: true,
},
optimization: {
runtimeChunk: "single",
}
};
};
const config = (env: WebpackBuildEnv): webpack.Configuration => {
const mode = env.mode;
const isSvg = env.svg;
const port = env.port || 3000;
const isDev = mode === "development";
const paths: WebpackBuildPaths = {
src: path.resolve(__dirname, "src"),
output: path.resolve(__dirname, "build"),
pages: path.resolve(__dirname, "src", "pages"),
svgo: path.resolve(__dirname, "config", "svgo", "svgo.config.ts"),
public: path.resolve(__dirname, "public"),
};
return buildWebpackConfig({
isDev,
isSvg,
mode,
port,
paths,
});
};
export default config;
Package.json:
{
"name": "krep-comp",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"build": "webpack --env mode=production --env svg && webpack --env mode=production"
},
"keywords": [],
"license": "ISC",
"devDependencies": {
"@types/node": "^20.8.4",
"@types/webpack": "^5.28.3",
"css-loader": "^6.8.1",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.3",
"mini-css-extract-plugin": "^2.7.6",
"sass": "^1.69.2",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"svg-chunk-webpack-plugin": "^4.0.2",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"twig-loader": "^0.5.5"
}
}
Project`s structure:
├── webpack.config.ts
├── package.json
├── src
| ├── app
| | ├── main
| | | ├── template.twig
| | | ├── script.ts
| ├── shared
| | ├── assets
| | | ├── images
| | | | ├── test.jpg
Twig tempalte
<!doctype html>
<html lang="{% block lang %}ru{% endblock %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{% block title %}{% endblock %}</title>
{% for js in htmlWebpackPlugin.files.js %}
{% if js|split('.')[0] == htmlWebpackPlugin.options.filename|split('.')[0] %}
<script defer="defer" src="{{ js }}"></script>
{% endif %}
{% endfor %}
{% for css in htmlWebpackPlugin.files.css %}
{% if css|split('.')[0] == htmlWebpackPlugin.options.filename|split('.')[0] %}
<link href="{{ css }}" rel="stylesheet">
{% endif %}
{% endfor %}
</head>
<body>
{% block content %}{% endblock %}
<img src="/src/shared/assets/images/test.jpg">
{% for sprite in htmlWebpackPlugin.files.svgSprites %}
{% if sprite[0]|split('.')[0] == htmlWebpackPlugin.options.filename|split('.')[0] %}
{{ sprite[1] }}
{% endif %}
{% endfor %}
</body>
</html>
To handle source files of scripts, styles, images, etc. in the
twig
template, you can use the HTML Bundler Plugin for Webpack. This plugin resolves references of assets in any template including the Twig.For example, there is a simple twig template:
The simple Webpack config:
The plugin can detect templates in a path automatically.
For advanced use case, e.g. if you want dynamically generate a custom output html filename based on dirname, you can use the filename entry option as a function.
The generated HTML will be looks like:
The plugin can inline SVG as content and images as base64 into HTML.
This plugin resolves source files in templates (like html-loader), extracts CSS (like mini-css-extract-plugin), JS, renders Twig templates. So, additional plugins and loaders such as html-webpack-plugin, mini-css-extract-plugin, html-loader, twig-loader are not required.