Требуется Webpack, батарейки в комплект не входят

Большинство приложений Angular будут работать при сборке из коробки, npx ng build и вуаля!
Что если нам потребуются дополнительные изменения в коде, которые зависят, например, от того, когда сборка сделанный? А вот и волшебство…

Примечание от автора: я никогда не верил в короткие рассказы (и моя жена это ненавидит), рассказы всегда длинные и должны объяснить весь процесс, иначе НИКАК не будет понятно, почему что-то происходит (или делается) именно так, а не иначе.
Если вы хотите пропустить длинная история, ctrl+F вот в чем подвох

Во-первых, давайте посмотрим на контекст

Допустим, у нас есть корпоративное приложение с некоторыми особыми требованиями к развертыванию, если мы их пропустим, это не принесет «никакого вреда» (в кавычках, поскольку мы все знаем, что это не совсем так), но имея их желательно.

Большинство из этих желаемых требований являются заменами внутри кода или созданием специального файла:

  • Оба файла index.html и environment.ts требуют замены заполнителя для развернутой версии или чего-то, что может помочь определить, какой выпуск был развернут, например хэш последней фиксации.
  • Развернутый проект требует определенного идентификатора для приложения отчетов об ошибках. Более того, все проекты живут в монорепозитории, поэтому операции отчетов об ошибках находятся во внутренней библиотеке, поэтому ее можно использовать в каждом другом проекте.
  • Внешний интерфейс приложения развернут в CDN (например, Cloudflare), и для использования преимущества кэширования некоторых редко обновляемых файлов стилей CSS копия бесхешированного-файла каждого CSS загружается на используемый VPS. как CDN для изображений и других статических файлов, которые обычно не изменяются. Это следует делать только для производственных развертываний.
  • Организация обнаружила несколько сторонних проектов на Github, которые удаляли общедоступный сайт, чтобы загружать определенные файлы и информацию без надлежащего разрешения (у меня есть не очень-очень-красивая- недружественный термин для этого). Чтобы усложнить эту операцию (или сделать ее достаточно сложной, чтобы помешать будущей работе по корректировке стратегии удаления), существуют некоторые случайно сгенерированные строки CSS и блоки HTML, которые предназначены для вставки в некоторые внутренние компоненты. (Да, я знаю, что это не настоящая мера безопасности, но когда усилия по решению простой задачи превышают ожидания, правда в том, что мы, скорее всего, сдадимся).
  • .map требуются для отслеживания ошибок и отладки (на стороннем сайте, предлагающем эту услугу), но при развертывании код должен быть в некоторой степени защищен. Чтобы добиться этого после загрузки файлов карты на сайт этого поставщика услуг, нам необходимо создать поддельный пустой файл карты. (Да, я также знаю, что лишь несколько вещей остановят любопытных разработчиков от проверки/попыток изучить код, но вы знаете, время – деньги, и если мы тратим впустую больше времени, которое нам платят за попытки решить что-то, что должно быть «просто», полезно еще раз спросить себя, стоит ли это дополнительных усилий).
  • Не свернутые файлы Javascript в папке /js должны быть удалены.

Как все это сделать?

Перед этим мы должны определить, какие операции нужно выполнить до сборки приложения, а какие — после, а также постараться расположить каждую задачу в каком-то порядке выполнения.

Перед сборкой:

  1. Заменить хеш в файлах index.html и environment.ts.
  2. Замена заполнителя идентификатора проекта во внутренней библиотеке для отчетов об ошибках.
  3. Замена антикоррозийных компонентов в компонентах.

После сборки:

  1. Скопируйте и загрузите безхешированный CSS
  2. Генерация поддельного файла карты
  3. Удалить несвернутый JS из папки /js

Мы могли бы сделать это, используя простые сценарии оболочки, но не секрет, что многие разработчики боятся командной строки и токсичных команд, таких как:

find . -name '*.*.css' -type f | awk -F. '{ print $2 }' | xargs -I{} bash -c 'cp .{}*.css .{}.css' 2> /dev/null || :

что объясняется следующим образом: найти все хешированные файлы css и сделать нехешированную копию, если команда не удалась, продолжайте работать.
Думаю, ChatGPT будет лучшей однострочной командой для этой подсказки 😅

Итак, мы решаем это с помощью webpack.

Почему и как вебпак?

Webpack — это инструмент для сборки модулей, используемый Angular для создания распространяемых файлов сборки. Для этого есть более быстрые, простые и лучшие инструменты, но мы все знаем, что webpack уже работает с Angular. Поскольку в большинстве случаев нам не нужно делать странные вещи в процессе сборки приложения, мы можем использовать его напрямую.

Но поскольку мы ДЕЙСТВУЕМ собираемся делать странные вещи, нам придется обогатить текущую конфигурацию веб-пакета, используемую Angular, дополнительным кодом, который решает наши требования. И в большинстве случаев это можно сделать напрямую, используя один из многих доступных плагинов для веб-пакетов, просто убедитесь, что вы выбрали правильный в соответствии с версией веб-пакета и, надеюсь, поддерживаемой библиотекой.

На самом деле есть 2 надежных способа выполнения этих операций:

  1. Если в вашем проекте используется nx-cli в качестве оболочки/замены ng-cli , вы можете использовать плагин Nx Webpack.
  2. Если вы не используете Nx (даже если используете), вы можете использовать пакет @angular-builders

Как?

С Nx это довольно просто:

// shamelessly copied & pasted from https://nx.dev/packages/webpack/documents/webpack-config-setup
"my-app": {
    "targets": {
        //...
        "build": {
            "executor": "@nx/webpack:webpack",
            //...
            "options": {
                //...
                "webpackConfig": "apps/my-app/webpack.config.js"
            },
            "configurations": {
                ...
            }
        },
    }
}

просто измените исполнителя сборки для проекта (в данном случае с именем my-app) внутри файла project.json.

Использовать пакет @angular-builder/custom-webpack очень просто:

// another shameless copy & pasted, this time from https://github.com/just-jeb/angular-builders/tree/master/packages/custom-webpack
"example-app": {
    ...
    "architect": {
      ...
      "build": {
        // the example uses the builder attribute, so this may change
        "executor": "@angular-builders/custom-webpack:browser",
        "options": {
          ...
          "customWebpackConfig": {
            "path": "apps/example-app/webpack.config.js",
            ...
          },
       }
    }
}

Обязательно загрузите правильную версию пакета, так как она имеет небольшие изменения в зависимости от версии Angular, которую использует ваш проект. Настройка аналогична примеру Nx.

Лично я предпочитаю пакет @angular-builders из-за поддержки некоторых дополнительных параметров. Я вернусь к этому, когда буду объяснять, как выполнить наши конкретные требования.

История сборки Angular и Webpack

Если мы перейдем к первому запуску и попытаемся решить все странные требования, мы действительно сможем создать файл webpack.config, который, вероятно, будет работать… в проекте, отличном от Angular. Это связано с тем, что Angular, начиная с версии 8, сделал больше, чем пару изменений в процессе сборки (узнал об этом, копаясь в документах и проблемах @angular-builders в их репозитории Github, а также методом проб и ошибок).

Итак, приступим к кодированию:

1. Заменить хеш в файлах index.html и environment.ts

Для этого мы будем использовать git-revision-webpack-plugin
И требуемый код будет примерно таким:

const webpack                 = require('webpack');
const { GitRevisionPlugin }   = require('git-revision-webpack-plugin');
const StringReplacePlugin     = require("string-replace-webpack-plugin");

const gitRevisionPlugin = new GitRevisionPlugin();
let currentCommit       = JSON.stringify(gitRevisionPlugin.commithash());
// Next line is part of the no-likes from this library
currentCommit           = currentCommit.replace(/["']+/g, '');

module.exports = (config, _options) => {
  let plugins = [
    gitRevisionPlugin,
    new StringReplacePlugin(),
  ];

  config.module.rules.unshift(
    {
      test:  /\.(html|ts)$/,
      use: [
        {
          loader: StringReplacePlugin.replace({
            replacements: [{
              pattern: /__COMMIT__/g,
              replacement: function (_match, _p1, _offset, _string) {
                return currentCommit;
              }
            }]
          })
        }
  );

  config.plugins.push(
    ...plugins
  );

  return config;
};

Мы также будем использовать string-replace-webpack-plugin для замены заполнителя __COMMIT__. Есть много других плагинов webpack для замены строк, но, несмотря на то, что этот довольно старый, он работал довольно хорошо.

Что мне не нравится в GitRevisionPlugin, так это то, что, поскольку вся информация о коммите находится в объекте JSON, для извлечения ее необходимо преобразовать в строку, а результирующая строка заключенный между кавычками. Это причина currentCommit.replace(/["']+/g, ''); line, поэтому мы можем получить 'чистую строку без кавычек'.

Обратите внимание, что правило замены в файле веб-пакета не изменено, а это означает, что оно будет помещено в качестве первого правила.
И плагины помещаются в массив плагинов, чтобы быть включенными последними.

Поскольку массивы знают о позиции, присвоенной каждому объекту, мы можем, по крайней мере, думать, что использование массива связано с тем, что позиции имеют значение (что первое, что последнее).
Итак, первое правило — заменить местозаполнитель.

Теоретически пока все хорошо.

2. Замена заполнителя идентификатора проекта во внутренней библиотеке для отчетов об ошибках.

Допустим, мы используем Sentry в качестве внешней службы отчетов об ошибках.
Из документации, чтобы узнать, какое приложение отправляет записи журнала, Sentry нужно имя выпуска, поэтому мы будем использовать заполнитель в нашем коде, назовем его __SENTRY_RELEASE__, и заменим так же, как мы сделали это с заполнителем __COMMIT__.

(надеюсь, вы не ожидаете, что я скопирую приведенный выше код из замены коммита и заменю __COMMIT__ на __SENTRY_RELEASE__)

Теоретически пока все так хорошо.

3. Замена антискрипных компонентов в компонентах

Некоторые компоненты включают случайно сгенерированную строку CSS, поэтому скребки, которые полагаются на сопоставление классов CSS, перестают работать. Как я уже упоминал, это не настоящая мера безопасности, но она немного затруднит любые неудачные попытки удаления сайта.

Заполнитель для этого случайного CSS — zcrappingNoMoor (достаточно креативно, чтобы кто-то другой не переписал эти стили).

Итак, простой SCSS, подобный этому:

:host {
  .product-card {
     // many other styles here
  }
}

станет:

:host {
  .product-zcrappingNoMoor-card {
     // many other styles here
  }
}

и при замене каждая компиляция будет иметь другой класс, например product-e6faaa67-card исправление логики некоторых разборщиков.

Как вы генерируете эту случайную строку, зависит от ваших предпочтений. То, что я сделал, немного излишне, но работает.

const { createHash }          = require('node:crypto');

// Random string based on the currrent commit hash
// Random size between 1 and 9 chars
const cssRndIdx = Math.floor(Math.random() * 8) + 1;
let cssRndStr   = createHash('md5').update(currentCommit).digest('hex');
cssRndStr       = cssRndStr.substring(0, cssRndIdx);

А потом еще один блок замены строки.
Теоретически пока все хорошо.

4. Скопируйте и загрузите безхешированный CSS (после сборки)

Если ваши стили не меняются 'так сильно', может быть (только может быть), было бы неплохо загрузить файл без хеширования styles.css в CDN. Таким образом, вы пропустите очистку кеша и будете использовать кеш браузера или CDN.

Здесь есть 2 момента:

  1. Фильтрация всех сгенерированных файлов CSS для удаления хэша из имени файла и копирование файла с этим безхешированным именем.
  2. Выполнение задачи после сборки.

Во-первых, даже ChatGPT может сгенерировать удобную функцию NodeJS для извлечения встроенных файлов, просто имейте в виду, что вам придется протестировать ее, чтобы убедиться, что вы получаете то, что вам нужно. И фильтрацию тоже можно делать разными способами.
Второй пункт имеет свои недостатки, так как задача должна выполняться ПОСЛЕ сборки.

Чтобы контролировать выполнение задачи веб-пакета, мы будем использовать Плагин Webpack Shell Next. Что мне действительно нравится в этом плагине, так это то, что вы можете выполнять как команды оболочки, так и функции javascript.

Чтобы загрузить файлы CSS на сервер CDN, мы будем использовать команду scp command. Я искал 100%-ную реализацию NodeJS для scp, но нашел только одну библиотеку, которая реализовала scp как scp , а не sftp (разные вещи).

scp означает "безопасная копия" и позволяет копировать файлы и папки на сервер SSH и с него. Поскольку мы выполняем эту команду без присмотра, нам потребуется способ пропустить запрос пароля, и для этого мы будем использовать SSH-ключ.
Командная строка, которую мы будем использовать для загрузки файл через scp имеет следующий вид:

scp -i ${pemKeyPath} -Cr ${cssFile} ${cdnUser}@${cdnServer}:${cdnCssPath}

где:

  • -i ${pemKeyPath} : ключ ssh, используемый для подключения. Помните, что открытый ключ должен быть загружен на сервер, иначе это не сработает. Ссылка: man ssh-copy-id command.
  • -Cr ${cssFile} : -Cдля использования сжатия для более быстрой передачи файлов, -r для работы в рекурсивный способ, означающий, что будут переданы все файлы, соответствующие файловому BLOB-объекту, cssFile — это именно тот файл, который мы хотим загрузить.
  • ${cdnUser}@${cdnServer}:${cdnCssPath} :Информация о соединении – это первая часть перед двоеточием (:), вторая часть (после двоеточия) — это путь, по которому будет храниться файл или файлы.

Трюк с командой scp заключается в том, что мы будем генерировать одну командную строку для каждого файла (чтобы избежать одиночной строки linerfind с командой _24 или конвейера с командой scp ,, которую мы могли бы построить непосредственно из оболочки).

И чуть не забыл, загрузку следует выполнять только для производственных сборок, поэтому нам нужно узнать среду компиляции из команды сборки.
Конфигурации могут можно найти в файлах angular.json или project.json внутри целевого объекта сборки под ключом JSON 'configurations'.

const webpack                 = require('webpack');
const WebpackShellPluginNext  = require('webpack-shell-plugin-next');

const fs   = require('fs');
const path = require('path');

// Default build folder and app name
const DIST_FOLDER = 'dist/apps/frontend';
const APP_NAME    = 'my-app';

// Avoid errors if the DIST_FOLDER does not exist
// Use the output-path parameter from the nx/ng build command line
let distFolderPath = path.join(process.cwd(), `${DIST_FOLDER}`);
const outputPathParam = JSON.parse(process.argv[2]).overrides?.['output-path'];
if (outputPathParam) {
  if (!outputPathParam.startsWith('.')) {
    distFolderPath = path.resolve(outputPathParam);
  } else {
    distFolderPath = path.join(process.cwd(), `${outputPathParam}`);
  }
} else {
  if (!fs.existsSync(distFolderPath)) {
    console.error(`Folder ${distFolderPath} does not exist!`);
    console.error(`Please run the build command from the projects root folder.`);

    process.exit(1);
  }
  distFolderPath = path.join(distFolderPath, `${APP_NAME}`);
}
console.log('distFolderPath: ', distFolderPath);

function getAllFiles(basePath) {
  let arrAllFiles = [];
  // Function to get the array of all available files from the basePath
  // probably a recursive function.
  return arrAllFiles;
}

// Some required variables
let allBuildFiles    = [];
let unhashedCssFiles = []; 

// Get the compile environment from the  command line
// Uncomment the console.log to understand the parameters
// console.log('Params: ', process.argv);
const configuration      = JSON.parse(process.argv[2]).targetDescription?.['configuration'] ?? '---';
const compileEnvironment = ( JSON.parse(process.argv[2]).targetDescription?.['target'] === 'build'
  && [ 'production', 'any-other-production-configuration' ].some(conf => configuration.indexOf(conf) !== -1) )
  ? 'production' : 'development';
console.log('configuration - compileEnvironment: ', configuration, ' - ', compileEnvironment);

const pemKeyPath = path.resolve('.', 'cdn_key.pem');
const cdnUser    = 'mycdnuser';
const cdnServer  = 'mycdn.domain.tld'; // can be an URL or an IP
const cdnCssPath = '/opt/www/cdn/styles'; // remote path on cdnServer

module.exports = (config, _options) => {
  let plugins = [
    new WebpackShellPluginNext({
      onBeforeBuild: {
        scripts: [ 'echo "Starting webpack build... "' ],
        blocking: true,
        parallel: false
      },
      onBuildEnd  : {
        scripts: [
          () => {
            allBuildFiles = getAllFiles(distFolderPath);
          },
          () => {
            // Copy all css files without hash
            unhashedCssFiles = [];
            const cssFiles = allBuildFiles.filter(file => file.endsWith('.css'));
            cssFiles.forEach(cssFile => {
              // The 2nd match group in the regexp is the hash we are removing
              const cssFileWithoutHash = cssFile.replace(/([\w\d-]+)\.([\w\d-]+).css/g, '$1.css');
              fs.copyFileSync(cssFile, cssFileWithoutHash);
              unhashedCssFiles.push(cssFileWithoutHash);
            });
          },
          // scp is a shell command
          ...( (compileEnvironment === 'production')
            ? unhashedCssFiles.map(cssFile => `scp -i ${pemKeyPath} -Cr ${cssFile} ${cdnUser}@${cdnServer}:${cdnCssPath}`)
            : [ `echo 'CSS files are only uploaded to the CDN on production builds'` ] ),
          // echo is a shell command :)
          'echo "Ending webpack build..."'
        ],
        blocking: true,
        parallel: false
      },      
    })
  ];
};

Это было не так просто.
И все же, пока все хорошо, по крайней мере, в теории…

Вы могли заметить, все работает… в теории… Код хороший, он должен работать. Никаких ошибок кодирования.

5. Генерация поддельного файла карты (после сборки)

Когда SPA или любое другое веб-приложение "скомпилировано", в большинстве случаев вы получите пакет JS (или несколько файлов JS) и столько же .js.map файлов.

Эти файлы карты предназначены для отладки. Ваши уродливые и нечитаемые (для людей) .js files при проверке в консоли разработки браузера на самом деле будут показывать код каждой функции.
Это хорошо для разработки, но не так хорошо, когда веб-приложение запущено в производство, как может раскрыть любую бизнес-логику в коде (или, если ответственный разработчик был недостаточно осторожен, даже более конфиденциальную информацию, такую ​​как токены API, например.)

С этими картографическими файлами можно проделать еще несколько трюков, но мы просто создадим фальшивый картографический файл, просто чтобы позабавить любопытных глаз. (Можно еще отлаживать запутанный код, но без выразительных имен функций и переменных это будет довольно сложно).

Файл поддельной карты, который мы создаем, будет иметь следующее содержимое:

{"version":3,"file":"/filename.<ugly hash>.js.map","sources":[],"names":[],"mappings":[]}

Пусто, нет ни переменных, ни ссылок на функции. И у каждого файла JavaScript есть один. Поскольку после сборки будут сгенерированы поддельные файлы карт, мы добавим generateFakeMap() as еще один скрипт onBuildEnd.

const webpack                 = require('webpack');
const WebpackShellPluginNext  = require('webpack-shell-plugin-next');

function generateFakeMap(filename) {
  return `{"version":3,"file":"/${fileName}","sources":[],"names":[],"mappings":[]}`;
}

module.exports = (config, _options) => {

  let plugins = [
    new WebpackShellPluginNext({
      onBeforeBuild: {
        scripts: [ 'echo "Starting webpack build... "' ],
        blocking: true,
        parallel: false
      },
      onBuildEnd  : {
        scripts: [
          () => {
            allBuildFiles = getAllFiles(distFolderPath);
          },
          () => {
            // Generate fake/empty map files
            const mapFiles = allBuildFiles.filter(file => file.endsWith('.map'));
            mapFiles.forEach(mapFile => {
              // filename array has fullPaths, so get only the filename without folders
              const fakeMap = mapFile.split('/').pop();
              // overwrite the  original mapFile
              fs.writeFileSync(mapFile, generateFakeMap(fileName));
            });
          }
        ]
      }
    })
  ];
  
  config.plugins.push(
    ...plugins
  );

  return config;
};

Ничего особенного, наш webpack.config по-прежнему немного прост и понятен. И пока все хорошо, в теории все еще «должно работать».

6. Удалить несвернутый JS из папки /js (после сборки)

И тут пришла беда (вот в чем загвоздка).

Включение такой функции в массив скриптов onBuildEnd должно работать:

          () => {
            // Delete all non-min.js files
            const jsFolder   = Object.keys(filesInFolders)?.filter(folder => folder.endsWith('/js'));
            const minJsFiles =  filesInFolders[jsFolder]?.filter(file => file.endsWith('.js') && !file.endsWith('.min.js'));
            minJsFiles?.forEach(minJsFile => {
              fs.unlinkSync(minJsFile);
            });
          },

Но… выучил и разобрался с этим трудным путем, и под трудным путем я имею в виду испорченную цепочку производственного развертывания и невозможность удалить не-минимальные файлы js.

Начиная с Angular 8 процесс сборки сильно изменился. Ресурсы и другие файлы, такие как пользовательские css, js или любые другие, которые, как ожидается, будут включены в распространяемый пакет, НЕ копируется или генерируется в процессе webpack и происходит снаружи, после его завершения.

Так что на самом деле не имело значения, были ли удаленные файлы, отличные от min.js, в onBuildEnd, или в onBuildExit, или в массивах скриптов onAfterDone. Во время сборки веб-пакета (во время выполнения процесса веб-пакета) не было найдено файлов, отличных от .min.js , соответствующих дополнительным ресурсам, которые я хотел включить в свой пакет. Единственные найденные файлы js были в результате компиляции, и их не хотелось удалять.

Это привело меня к двойной проверке моего с помощью веб-пакета процесса сборки. Шаг 1. (замена заполнителя хэша commit в index.html file) также завершился с ошибкой, как и шаг 3. (замена anti-scrapper) также не работала. Это было нехорошо.

Итак, снова вернулся к документации Пакет сборщиков Angular и поискал в проблемах.
На самом деле ничего не нашел о том, когда было сделано дополнительное копирование файлов. Но есть пара проблем, объясняющих, что файл index.html не создается в сборке webpack, поэтому любые изменения следует выполнять с помощью файла indexTranform.

С заменой анти-скраппера все не так просто. Атрибут directTemplateLoading в AngularCompilerPlugin должен иметь значение false (подробности объясняются здесь). Как и ожидалось, копирование и вставка предложенного кода не сработали (начиная с Angular v8 до текущей версии мы можем ожидать более пары изменений).

Так что подход должен измениться.

Фиксация уловов

1. Замена заполнителя хэша коммита в файле index.html

Если вы используете Angular builders webpack.config, всякий раз, когда вам нужно внести определенные коррективы в файл index.html file в вашем приложении Angular (например, заменить определенный заполнитель), вам придется делать это с помощью indexTransform.

В нашем случае:

// apps/example-app/indexTransform.ts
import { TargetOptions } from '@angular-builders/custom-webpack';
import { GitRevisionPlugin } from 'git-revision-webpack-plugin';

const gitRevisionPlugin = new GitRevisionPlugin();
let currentCommit       = JSON.stringify(gitRevisionPlugin.commithash());

export default (targetOptions: TargetOptions, indexHtml: string) => {
  console.log('\nindexTransform.ts: currentCommit: ', currentCommit);
  // console.log('\nindexTransform.ts: targetOptions: ', targetOptions);
  currentCommit = currentCommit.replace(/["']+/g, '');
  return indexHtml.replace(/__COMMIT__/g, currentCommit);
};

Код в основном тот же, что и в файле webpack.config. И мы сохраняем этот код, потому что в некоторых других файлах .ts есть пара заполнителей.

И в нашем файле project.json:

"example-app": {
    ...
    "architect": {
      ...
      "build": {
        // the example uses the builder attribute, so this may change
        "executor": "@angular-builders/custom-webpack:browser",
        "options": {
          ...
          "indexTransform": "apps/example-app/indexTransform.ts",
          "customWebpackConfig": {
            "path": "apps/example-app/webpack.config.js",
            ...
          },
       }
    }
}

После запуска сборки каждый заполнитель __COMMIT__ будет правильно заменен.

3. Замена антискребков

Как указывалось в упомянутой проблеме, мы должны изменить это значение directTemplateLoading: false в настройках AngularCompilerPlugin. Но мы не будем пытаться выяснить все остальные настройки для этого плагина. Поэтому мы просто заменим настройки webpack.config, сохранив позицию для AngularCompilerPlugin в массиве подключаемых модулей.

Для Angular 15 я обнаружил не AngularCompilerPlugin, а AngularWebpackPlugin. Ожидал…

  // ... rest of your webpack.config

  const index = config.plugins.findIndex(p => {
    let isFound = false;
    try {
      isFound = p.constructor.name === 'AngularWebpackPlugin';
    } catch (error) {
      console.error('Plugin without constructor: ', error);
    }
    return isFound;
  });
  // Change the existing AngularWebpackPlugin, keep old options...
  const oldOptions                 = config.plugins[index].pluginOptions;
  // ... except the directTemplateLoading value
  oldOptions.directTemplateLoading = false;
  config.plugins[index]            = new AngularWebpackPlugin.AngularWebpackPlugin(oldOptions);

  return config;
}

В нашем файле project.json нам нужно добавить флаг, чтобы у нас не было огромного повторяющегося беспорядка в наших плагинах webpack ( replaceDuplicatePlugins: true):

"example-app": {
    ...
    "targets": {
      ...
      "build": {
        // the example uses the builder attribute, so this may change
        "executor": "@angular-builders/custom-webpack:browser",
        "options": {
          ...
          "indexTransform": "apps/example-app/indexTransform.ts",
          "customWebpackConfig": {
            "path": "apps/example-app/webpack.config.js",
            "replaceDuplicatePlugins": true
            ...
          },
       }
    }
}

С этой модификацией наша замена будет работать для каждого файла .html в наших компонентах.

6. Удалить несвернутый JS из папки /js

Наконец-то…
Пришлось посмотреть на эту проблему с разных сторон, и ответ был не в коде, а в настройках.

Поскольку в этом конкретном случае файлы .min.js не являются результатом компиляции скриптов, а просто включают некоторые другие скрипты в пакет dist, мы можем указать Angular включать только файлы .min.js, пропуская не минимизированные версии.

Итак, в нашем файле project.json:

"example-app": {
    ...
    "targets": {
      ...
      "build": {
        // the example uses the builder attribute, so this may change
        "executor": "@angular-builders/custom-webpack:browser",
        "options": {
          ...
          "indexTransform": "apps/example-app/indexTransform.ts",
          "customWebpackConfig": {
            "path": "apps/example-app/webpack.config.js",
            "replaceDuplicatePlugins": true,
            ...
          },
          "assets": [
            {
              "input": "libs/frontend/src/lib/assets",
              "glob": "**.min.js",
              "output": "js"
            }
          ],
          ...
          
       }
    }
}

Таким образом, используя шаблон glob«**.min.js», мы гарантируем, что наша компиляция Angular включает только свернутые файлы Javascript, пропуская не свернутые файлы, поэтому удаление файлов не требуется.

Спасибо, что дочитали до этого места.
Это была очень длинная история, и я надеюсь, вы поняли, как вносить коррективы в процесс сборки проектов Angular с помощью webpack.

В большинстве случаев вам не нужно выполнять какие-либо из этих задач, но если вам нужно решить какое-либо особое требование до или после сборки, webpack может быть ответом.

Будьте в безопасности 👍