Node.js сбой во время сборки углового 6 с --watch

Я использую Node v8.12.0 на Mac (хотя я видел эту проблему с версиями Node 9.x, а также в Linux).

Я разрабатываю приложение Angular 6 и запускаю dev с --watch флага --watch. Часы будут работать и могут перестроить приложение, возможно, 4 или 5 раз, затем Node выйдет со следующим выходом:

<--- Last few GCs --->

[34201:0x104000000]   273927 ms: Mark-sweep 1309.4 (1430.5) -> 1309.2 (1431.0) MB, 1296.0 / 0.0 ms  allocation failure GC in old space requested
[34201:0x104000000]   275358 ms: Mark-sweep 1309.2 (1431.0) -> 1309.2 (1424.0) MB, 1430.8 / 0.0 ms  last resort GC in old space requested
[34201:0x104000000]   276946 ms: Mark-sweep 1309.2 (1424.0) -> 1309.2 (1423.5) MB, 1587.7 / 0.0 ms  last resort GC in old space requested


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x1c5f3a825879 <JSObject>
1: fromString(aka fromString) [buffer.js:~298] [pc=0x2234a1ca140b](this=0x1c5ffcc022d1 <undefined>,string=0x1c5f6f8dffa1 <Very long string[784654]>,encoding=0x1c5ffcc022d1 <undefined>)
2: from [buffer.js:177] [bytecode=0x1c5f43e4aac9 offset=11](this=0x1c5f8a5b5c51 <JSFunction Buffer (sfi = 0x1c5f3a87e159)>,value=0x1c5f6f8dffa1 <Very long string[784654]>,encodingOrOffset=0x1c5ffcc022d1 <u...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: node::Abort() [/usr/local/bin/node]
 2: node::FatalException(v8::Isolate*, v8::Local<v8::Value>, v8::Local<v8::Message>) [/usr/local/bin/node]
 3: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/usr/local/bin/node]
 4: v8::internal::Factory::NewRawTwoByteString(int, v8::internal::PretenureFlag) [/usr/local/bin/node]
 5: v8::internal::String::SlowFlatten(v8::internal::Handle<v8::internal::ConsString>, v8::internal::PretenureFlag) [/usr/local/bin/node]
 6: v8::String::WriteUtf8(char*, int, int*, int) const [/usr/local/bin/node]
 7: node::StringBytes::Write(v8::Isolate*, char*, unsigned long, v8::Local<v8::Value>, node::encoding, int*) [/usr/local/bin/node]
 8: node::Buffer::New(v8::Isolate*, v8::Local<v8::String>, node::encoding) [/usr/local/bin/node]
 9: node::Buffer::(anonymous namespace)::CreateFromString(v8::FunctionCallbackInfo<v8::Value> const&) [/usr/local/bin/node]
10: 0x2234a02d4067
11: 0x2234a1ca140b
12: 0x2234a023d1d6
13: 0x2234a018535f

Я также попытался добавить --max_old_space_size=12000, но это, похоже, не имеет никакого значения. Я не уверен, где искать причину проблемы или как начать отладку в узле. Любая помощь была бы чрезвычайно оценена!

Некоторые предпосылки: это приложение Angular 5 с выталкиваемой конфигурацией, и я обновил его до Angular 6 с той же конфигурацией, и само приложение работает должным образом. Только с момента обновления к Angular 6 этот вопрос начал возникать.

Для справки, здесь находятся части зависимости и devDependency для package.json:

"dependencies": {
  "@angular/animations": "6.1.10",
  "@angular/cdk": "6.4.7",
  "@angular/common": "6.1.10",
  "@angular/compiler": "6.1.10",
  "@angular/core": "6.1.10",
  "@angular/forms": "6.1.10",
  "@angular/http": "6.1.10",
  "@angular/material": "6.4.7",
  "@angular/platform-browser": "6.1.10",
  "@angular/platform-browser-dynamic": "6.1.10",
  "@angular/router": "6.1.10",
  "@ng-idle/core": "6.0.0-beta.3",
  "@ng-idle/keepalive": "6.0.0-beta.3",
  "@ngrx/effects": "6.1.2",
  "@ngrx/entity": "6.1.2",
  "@ngrx/router-store": "6.1.2",
  "@ngrx/store": "6.1.2",
  "@ngrx/store-devtools": "6.1.2",
  "@swimlane/ngx-datatable": "14.0.0",
  "@types/crypto-js": "3.1.37",
  "@types/moment": "2.13.0",
  "angular2-toaster": "6.1.0",
  "angulartics2": "7.2.0",
  "core-js": "2.5.7",
  "crypto-js": "3.1.9-1",
  "hammerjs": "2.0.8",
  "immutable": "3.8.2",
  "jquery": "2.2.4",
  "moment": "2.19.1",
  "ng2-charts": "1.6.0",
  "ngx-zendesk-webwidget": "0.1.3",
  "node-waves": "0.7.6",
  "normalize.css": "3.0.3",
  "rxjs": "6.3.3",
  "sass": "1.15.1",
  "zone.js": "0.8.26"
},
"devDependencies": {
  "@angular-builders/custom-webpack": "7.0.0",
  "@angular-devkit/build-angular": "0.11.0",
  "@angular/cli": "7.0.6",
  "@angular/compiler-cli": "6.1.10",
  "@angular/language-service": "6.1.10",
  "@types/jasmine": "2.5.53",
  "@types/jasminewd2": "2.0.2",
  "@types/node": "6.0.60",
  "autoprefixer": "9.3.1",
  "chromedriver": "2.38.2",
  "clean-webpack-plugin": "1.0.0",
  "codelyzer": "4.5.0",
  "copy-webpack-plugin": "4.6.0",
  "css-loader": "1.0.1",
  "cssnano": "4.1.7",
  "exports-loader": "0.7.0",
  "file-loader": "2.0.0",
  "istanbul-instrumenter-loader": "2.0.0",
  "jasmine-allure-reporter": "1.0.2",
  "jasmine-core": "2.6.2",
  "jasmine-marbles": "0.4.0",
  "jasmine-spec-reporter": "4.2.1",
  "karma": "3.0.0",
  "karma-chrome-launcher": "2.2.0",
  "karma-cli": "1.0.1",
  "karma-coverage-istanbul-reporter": "2.0.1",
  "karma-jasmine": "1.1.2",
  "karma-jasmine-html-reporter": "0.2.2",
  "karma-spec-reporter": "0.0.32",
  "lint-staged": "8.1.0",
  "loader-utils": "1.1.0",
  "mini-css-extract-plugin": "0.4.5",
  "npm-run-all": "4.1.5",
  "postcss-custom-properties": "8.0.9",
  "postcss-loader": "3.0.0",
  "postcss-url": "8.0.0",
  "pre-commit": "1.2.2",
  "process": "0.11.10",
  "protractor": "5.4.1",
  "protractor-console": "3.0.0",
  "protractor-jasmine2-html-reporter": "0.0.7",
  "puppeteer": "1.6.0",
  "raw-loader": "0.5.1",
  "rxjs-tslint": "0.1.5",
  "sass-loader": "7.1.0",
  "selenium-server-standalone-jar": "3.8.1",
  "source-map-loader": "0.2.4",
  "style-loader": "0.23.1",
  "stylelint": "9.6.0",
  "stylelint-config-recommended": "2.1.0",
  "ts-mockito": "2.3.1",
  "ts-node": "3.2.0",
  "tslint": "5.7.0",
  "typescript": "2.9.2",
  "uglifyjs-webpack-plugin": "2.0.1",
  "url-loader": "1.1.2",
  "webpack": "4.24.0",
  "webpack-bundle-analyzer": "3.0.3",
  "webpack-cli": "3.1.2",
  "webpack-concat-plugin": "3.0.0",
  "webpack-dev-server": "3.1.10",
  "webpack-filter-warnings-plugin": "^1.2.1",
  "yargs": "8.0.1"
}

Наконец, здесь используется настраиваемая конфигурация webpack (которая работает безупречно в Angular 5):

const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const autoprefixer = require('autoprefixer');
const postcssUrl = require('postcss-url');
const cssnano = require('cssnano');
const customProperties = require('postcss-custom-properties');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const { NoEmitOnErrorsPlugin, SourceMapDevToolPlugin, NormalModuleReplacementPlugin } = require('webpack');
const { AngularCompilerPlugin } = require('@ngtools/webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const FilterWarningsPlugin = require('webpack-filter-warnings-plugin');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const ProgressPlugin = require('webpack/lib/ProgressPlugin');

const postcssPlugins = function (env) {

  // safe settings based on: https://github.com/ben-eb/cssnano/issues/358#issuecomment-283696193
  const importantCommentRe = /@preserve|@license|[@#]\s*source(?:Mapping)?URL|^!/i;
  const baseHref = '';
  const deployUrl = '';
  const minimizeOptions = {
    preset: [
      'default',
      {
        mergeLonghand: false,
        discardComments: { remove: (comment) => !importantCommentRe.test(comment) }
      }
    ]
  };
  return [
    postcssUrl({
      url: (URL) => {
        // Only convert root relative URLs, which CSS-Loader won't process into require().
        if (!URL.url.startsWith('/') || URL.url.startsWith('//')) {
           return URL.url;
        }
        if (deployUrl.match(/:\/\//)) {
          // If deployUrl contains a scheme, ignore baseHref use deployUrl as is.
          return '${deployUrl.replace(/\/$/, '')}${URL.url}';
        }
        else if (baseHref.match(/:\/\//)) {
          // If baseHref contains a scheme, include it as is.
          return baseHref.replace(/\/$/, '') +
        '/${deployUrl}/${URL.url}'.replace(/\/\/+/g, '/');
        }
        else {
          // Join together base-href, deploy-url and the original URL.
          // Also dedupe multiple slashes into single ones.
          return '/${baseHref}/${deployUrl}/${URL.url}'.replace(/\/\/+/g, '/');
        }
      }
    }),
    autoprefixer(),
    customProperties({ preserve: true })
  ].concat(env === 'prod' ? [cssnano(minimizeOptions)] : []);
};

const builder = (customer, prodEnv) => {

let plugins = [
    new ProgressPlugin(),
    new NoEmitOnErrorsPlugin(),
    new FilterWarningsPlugin({
      exclude: /System.import/
    }),
    new CleanWebpackPlugin(['target/classes/static/' + customer]),
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css'
    }),
    new CopyWebpackPlugin([
        {
            context: 'src/main/angular',
            to: '',
            from: {
                glob: 'assets/**/*',
                dot: true
            }
        },
        {
            context: 'src/main/angular',
            to: '',
            from: {
                glob: 'favicon.ico',
                dot: true
            }
        }
    ], {
        ignore: [
            '.gitkeep',
            '**/.DS_Store'
        ],
        debug: 'warning'
    }),
    //Replace the actual environment file with the correct one passed in via env args
    new NormalModuleReplacementPlugin(/(.*)\environments\/environment(\.*)/, function(resource) {
        resource.request = resource.request.replace('environments/environment',
            'environments/${customer}/environment.${prodEnv}');
    }),
    //Replace the actual chart-colors file with the correct one based on customer
    new NormalModuleReplacementPlugin(/(.*)\environments\/chart-colors.json/, function(resource) {
        resource.request = resource.request.replace('environments/chart-colors.json',
            'environments/${customer}/chart-colors.json');
    }),
    //Replace the actual lang file with the correct one based on customer
    new NormalModuleReplacementPlugin(/(.*)\environments\/lang.json/, function(resource) {
        resource.request = resource.request.replace('environments/lang.json',
            'environments/${customer}/lang.json');
    }),
    //Replace the actual scss file with the correct one based on customer
    new NormalModuleReplacementPlugin(/(.*)\environments\/styles.scss/, function(resource) {
        resource.request = resource.request.replace('environments/styles.scss',
            'environments/${customer}/styles.scss');
    }),
    new AngularCompilerPlugin({
        mainPath: 'main.ts',
        platform: 0,
        sourceMap: (prodEnv === 'dev') ? true : false,
        tsConfigPath: 'src/main/angular/tsconfig.app.json',
        skipCodeGeneration: true,
        compilerOptions: {}
    }),
    new webpack.ProvidePlugin({
        $: 'jquery',
        jQuery: 'jquery',
        "window.jQuery": 'jquery',
        Hammer: 'hammerjs/hammer'
    })
];

let devPlugins = [
    new CircularDependencyPlugin({
        exclude: /(\\|\/)node_modules(\\|\/)/,
        failOnError: false
    }),
    new SourceMapDevToolPlugin({
        filename: '[file].map[query]',
        moduleFilenameTemplate: '[resource-path]',
        fallbackModuleFilenameTemplate: '[resource-path]?[hash]',
        sourceRoot: 'webpack:///',
        exclude: ['vendor.js']
    }),
    new BundleAnalyzerPlugin({
        generateStatsFile: true
    })
];

let prodPlugins = [
    new UglifyJsPlugin({
        parallel: true,
        sourceMap: false
    })
];

plugins = prodEnv === 'dev'
    ? plugins.concat(devPlugins)
    : plugins.concat(prodPlugins);

return  {
  resolve: {
    extensions: [
      '.ts',
      '.js'
    ],
    modules: [
      './node_modules'
    ],
    symlinks: true,
    alias: {
      "rxjs/" : './node_modules/rxjs/_esm2015/'
    },
    mainFields: [
      'browser',
      'module',
      'main'
    ]
  },
  resolveLoader: {
    modules: [
      './node_modules'
    ]
  },
  entry: {
    main: [
      './src/main/angular/main.ts'
    ],
    polyfills: [
      './src/main/angular/polyfills.ts'
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: path.join(process.cwd(), 'node_modules'),
          name: 'vendor',
          enforce: true,
          filename: 'vendor.chunk.js'
        }
      }
    }
  },
  output: {
    path: path.join(process.cwd(), 'target', 'classes', 'static', customer),
    filename: '[name].bundle.js',
    chunkFilename: '[id].chunk.js',
    crossOriginLoading: false
  },
  module: {
    rules: [
      {
        test: /\.html$/,
        loader: 'raw-loader'
      },
      {
        test: /\.(eot|svg|cur)$/,
        loader: 'file-loader',
        options: {
          name: '[name].[hash:20].[ext]',
          limit: 10000
        }
      },
      {
        test: /\.(jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/,
        loader: 'url-loader',
        options: {
          name: '[name].[hash:20].[ext]',
          limit: 10000
        }
      },
      {
        test: /\.css$/,
        use: [
          'exports-loader?module.exports.toString()',
          {
            loader: 'css-loader',
            options: {
              sourceMap: false,
              importLoaders: 1
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              ident: 'postcss',
              plugins: postcssPlugins(prodEnv)
            }
          }
        ]
      },
      {
        test: /\.css$/,
        include: [
            path.join(process.cwd(), 'src/main/angular/environments/${customer}/styles.scss')
        ],
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              sourceMap: false,
              importLoaders: 1
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              ident: 'postcss',
              plugins: postcssPlugins(prodEnv)
            }
          }
        ]
      },
      {
        test: /\.scss$/,
        include: [
          path.join(process.cwd(), 'src/main/angular/environments/${customer}/styles.scss')
        ],
        use: [
            MiniCssExtractPlugin.loader,
            {
                loader: 'css-loader',
                options: {
                    sourceMap: false,
                    importLoaders: 1
                }
            },
            {
                loader: 'postcss-loader',
                options: {
                    ident: 'postcss',
                    plugins: postcssPlugins(prodEnv)
                }
            },
            {
                loader: 'sass-loader',
                options: {
                    sourceMap: false,
                    precision: 8,
                    includePaths: [path.join(process.cwd(), 'src', 'main', 'angular')]
                }
            }
          ]
        },
        {
          test: /\.ts$/,
          loader: '@ngtools/webpack'
        }
      ]
    },
    mode: (prodEnv === 'prod') ? 'production' : 'development',
    plugins: plugins,
    node: {
      fs: 'empty',
      global: true,
      crypto: 'empty',
      tls: 'empty',
      net: 'empty',
      process: true,
      module: false,
      clearImmediate: false,
      setImmediate: false
    },
    devServer: {
      historyApiFallback: true
    },
    watchOptions: {
      aggregateTimeout: 500
    }
  };
};

module.exports = {
  build: builder
}

Ответ 1

Это называется утечкой памяти, которая говорит, что вы пытаетесь зарезервировать огромную память !

Обратите внимание, что выделение большего объема памяти (--max_new_space_size и/или --max_old_space_size) не решит основной проблемы, хотя может помочь продолжить работу с приложением, потребляющим память.

Фон

Как вы знаете, в приложениях javascript сборка представляет собой процесс экспорта мини-пакетов, в которых они поступают из зависимостей и ваших индивидуальных кодов. В некоторых случаях проблемы совместимости между зависимостями (версии несоответствия) могут резервировать больше памяти! Например, пользователь, обнаруживший lodash v4.14.70, несовместим с TS 2.7 из коробки. Хотя вы не используете lodash, такой вопрос можно ожидать.

Кроме того, вы должны быть знакомы с случаями в javascript, где происходит утечка памяти, а затем выяснить, что использует слишком много памяти в вашем приложении. Я также рекомендую записывать снимки кучи. Я перечисляю некоторые случаи, когда может произойти утечка памяти (с первого взгляда):

  • Нажатие элементов в массив, но не освобождение или сброс массива
  • Прикрепление замыканий как функции обработчика событий-слушателей
  • Сохранение обратных вызовов в объект, который живет, даже если обратные вызовы больше не нужны
  • Хранение символов (с тем же именем) как свойств объекта
  • Постоянное создание новых свойств объекта (каждое с другим именем) без удаления предыдущих
  • Добавление свойств в Set, Map или WeakMap
  • Хранение неразрешенных обещаний в массиве
  • разбор огромного объекта JSON

отладка

Поскольку ваше приложение отлично работало до обновления до Angular6, основная привязанность исходит из ваших новых зависимостей. Я думаю, что это необходимо для очистки вашего проекта от неиспользуемых зависимостей в качестве первого шага, возможно, с помощью какого-то инструмента, такого как проверка зависимостей (я его никогда не тестировал). Затем попробуйте перенести проект с Angular5 на Angular6 снова с помощью Angular Update Guide. Затем проверьте совместимость между зависимостями. Во время написания этого ответа я не нашел никакого инструмента для поиска совместимых версий, но, как трюк, вы можете использовать версии, выпущенные в тот же период времени (одновременно).

Ответ 2

Мы видели, что это иногда случалось и с Angular 5. Наше решение (обходной путь?) - запустить ng serve с параметром max_old_space_size.

node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng serve --aot

настройка --max_old_space_size=8192 работает для меня, но это действительно хорошее решение для разработчиков из одной части. Для другого мы можем попробовать другое решение

Это мое решение, оно требует, чтобы люди использовали git bash в качестве терминала, если в Windows, но это было бы легко изменить при необходимости (просто используйте вместо этого файл cmd):

В корне моего проекта у меня есть папка с именем scripts и в ней файл с именем ng.sh, который является копией из node_modules/.bin/ng но с большей разрешенной оперативной памятью.

#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

case 'uname' in
    *CYGWIN*) basedir='cygpath -w "$basedir"';;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node" --max_old_space_size=8192 "./node_modules/@angular/cli/bin/ng" "[email protected]"
  ret=$?
else
  node --max_old_space_size=8192 "./node_modules/@angular/cli/bin/ng" "[email protected]"
  ret=$?
fi
exit $ret

Затем в моем package.json я делаю:

"scripts": {
    "build-prod": "bash ./scripts/ng.sh build --prod --aot --env=prod"
}