From 141b190a06a65524e3bf7c1a0b8c23804aa80705 Mon Sep 17 00:00:00 2001 From: Francisco J Lopez-Pellicer Date: Tue, 20 Oct 2026 15:04:04 +0200 Subject: [PATCH] fix(docker): build the viewer with the Angular 22 webpack config The image template was still the old object config, so docker compose build front failed on jszip, util, and @spz-loader. --- CHANGELOG.md | 1 + front/Dockerfile | 2 +- front/viewer/webpack.config.js.template | 147 ++++++++++++++++++++++-- 3 files changed, 137 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85fc7f6..fc90525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### Stack-level +- **Docker**: `docker compose build front` uses the Angular 22 viewer webpack config. `envsubst` replaces only `PUBLIC_BASE_PATH`, so the viewer image build keeps the `jszip`, `util`, and `@spz-loader` exclusions. - **Docker**: `sitmun-shared` checks out `gradlew` with LF line endings. On Windows with `core.autocrlf=true`, the backend and proxy image builds failed in `publishToMavenLocal` with `/bin/sh^M: bad interpreter` (exit code 126) ([#89](https://github.com/sitmun/sitmun-application-stack/issues/89)). - **E2E**: Each root E2E job publishes the pinned `sitmun-shared` to the local Maven repository before the suite starts. The backend resolves `sitmun-spring-upstream` and `sitmun-proxy-contract` `1.2.9` from that repository. - **Submodules**: `sitmun-mbtiles` follows the stack branch. `git submodule update --remote` on stack `dev` stays on mbtiles `dev`, the commit the stack already pins. diff --git a/front/Dockerfile b/front/Dockerfile index 11e626b..ddd87b5 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -55,7 +55,7 @@ RUN export BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z") && \ if [ "${ENVIRONMENT}" = "production" ]; then export FRONT_ENV=Production; export FRONT_PRODUCTION=true; else export FRONT_ENV=Development; export FRONT_PRODUCTION=false; fi && \ envsubst '$BUILD_TIMESTAMP,$APP_VERSION,$FRONT_ENV,$PUBLIC_BASE_PATH' < ./src/environments/environment.prod.ts.template > ./src/environments/environment.prod.ts RUN envsubst < ./src/index.html.template > ./src/index.html -RUN envsubst < ./webpack.config.js.template > ./webpack.config.js +RUN envsubst '$$PUBLIC_BASE_PATH' < ./webpack.config.js.template > ./webpack.config.js RUN if [ "${ENVIRONMENT}" = "development" ]; then \ npm run build -- --configuration=docker-dev --base-href=${PUBLIC_BASE_PATH}viewer/; \ else \ diff --git a/front/viewer/webpack.config.js.template b/front/viewer/webpack.config.js.template index 1cb7fcd..1731cd6 100644 --- a/front/viewer/webpack.config.js.template +++ b/front/viewer/webpack.config.js.template @@ -1,28 +1,151 @@ const webpack = require('webpack'); +const path = require('path'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); + const apiSitnaSource = 'node_modules/api-sitna'; -const apiSitnaDestiny = '${PUBLIC_BASE_PATH}viewer/assets/js/api-sitna'; -const path = require("path"); +const apiSitnaDestiny = 'assets/js/api-sitna'; + +// Angular 22 always passes supportedBrowsers into its Babel loader, so matched +// dependencies are transformed. jszip/util crash that path under optimize. +const skipAngularBabel = /[\\/]node_modules[\\/](?:jszip|util)[\\/]/; + +function ruleUsesAngularBabel(rule) { + const entries = []; + if (typeof rule.loader === 'string') { + entries.push(rule.loader); + } + if (Array.isArray(rule.use)) { + for (const use of rule.use) { + if (typeof use === 'string') { + entries.push(use); + } else if (use && typeof use.loader === 'string') { + entries.push(use.loader); + } + } + } + return entries.some( + (loader) => + loader.includes(`${path.sep}babel${path.sep}webpack-loader`) || + loader.includes('@angular-devkit/build-angular/src/tools/babel') + ); +} + +function appendExclude(rule, excludeRe) { + const existing = rule.exclude; + if (!existing) { + rule.exclude = [excludeRe]; + return; + } + if (Array.isArray(existing)) { + if (!existing.includes(excludeRe)) { + rule.exclude = [...existing, excludeRe]; + } + return; + } + rule.exclude = [existing, excludeRe]; +} + +function excludeFromAngularBabel(rule) { + if (!rule || typeof rule !== 'object') { + return; + } + if (Array.isArray(rule.oneOf)) { + rule.oneOf.forEach(excludeFromAngularBabel); + } + if (Array.isArray(rule.rules)) { + rule.rules.forEach(excludeFromAngularBabel); + } + if (ruleUsesAngularBabel(rule)) { + appendExclude(rule, skipAngularBabel); + } +} -module.exports = { +module.exports = (config) => { + config.output = { + ...config.output, + publicPath: '${PUBLIC_BASE_PATH}viewer/' + }; - resolve: { - // Evita errores del tipo "Module not found" durante el empaquetamiento + const existingProxy = config.devServer && config.devServer.proxy; + const proxyEntries = Array.isArray(existingProxy) + ? [...existingProxy] + : existingProxy + ? Object.entries(existingProxy).map(([context, options]) => ({ + context: [context], + ...options + })) + : []; + proxyEntries.push({ + context: ['/catastro-inspire'], + target: 'https://ovc.catastro.meh.es', + changeOrigin: true, + pathRewrite: { '^/catastro-inspire': '/INSPIRE' }, + secure: false + }); + config.devServer = { + ...config.devServer, + hot: false, + liveReload: false, + client: false, + proxy: proxyEntries + }; + + config.resolve = { + ...config.resolve, fallback: { + ...(config.resolve && config.resolve.fallback), assert: false, util: false, process: require.resolve('process/browser') + }, + alias: { + ...(config.resolve && config.resolve.alias), + '@spz-loader/core': path.resolve(__dirname, 'src/shims/empty-spz-loader.js') + } + }; + + const assetsPath = path.resolve(__dirname, 'src/assets'); + const previousNoParse = config.module && config.module.noParse; + config.module = { + ...config.module, + noParse: (filePath) => { + if (skipAngularBabel.test(filePath)) { + return true; + } + if (filePath.includes(assetsPath) && filePath.endsWith('.js')) { + return true; + } + if (typeof previousNoParse === 'function') { + return previousNoParse(filePath); + } + if (Array.isArray(previousNoParse)) { + return previousNoParse.some((re) => + re instanceof RegExp ? re.test(filePath) : false + ); + } + if (previousNoParse instanceof RegExp) { + return previousNoParse.test(filePath); + } + return false; } - }, + }; + + if (Array.isArray(config.module.rules)) { + config.module.rules.forEach(excludeFromAngularBabel); + } - plugins: [ - // Procesado de polyfill + config.plugins = [ + ...(config.plugins || []), new webpack.ProvidePlugin({ process: 'process/browser' }), - - // Define la ruta base de la API SITNA para la carga de recursos + new CopyWebpackPlugin({ + patterns: [{ from: apiSitnaSource, to: apiSitnaDestiny }] + }), new webpack.DefinePlugin({ - SITNA_BASE_URL: JSON.stringify(apiSitnaDestiny) + SITNA_BASE_URL: JSON.stringify('${PUBLIC_BASE_PATH}viewer/assets/js/api-sitna/') }) - ], + ]; + + return config; };