webpack-encore icon indicating copy to clipboard operation
webpack-encore copied to clipboard

Question : Proper way to handle angular 2 or 4

Open neovea opened this issue 7 years ago • 24 comments

Hi there, I'm willing to work with symfony and webpack encore, and add angular 4 to it. Is there any proper way to handle this ?

Thanks

neovea avatar Jul 13 '17 09:07 neovea

I'd be very happy to add Angular support :). I haven't looked into it at all yet and I don't use Angular. For Vue, it was of adding the vue-loader - #49. For react, it involved add a babel preset. I imagine that API would be something like enableAngularLoader() (assuming it needs a loader), but I don't know what is needed in Webpack for Angular.

In other words, PR very welcome - even if it's just a work-in-progress PR.

weaverryan avatar Jul 13 '17 15:07 weaverryan

Use the angular CLI and then run ng eject after starting a new project, that'll output the Webpack config.
Learned that in the Gitter chat.

I've asked if the guys from the Angular Gitter.im chat if they could take a peek at this, so fingers crossed! :)

Coffee2CodeNL avatar Jul 29 '17 09:07 Coffee2CodeNL

Here's a default output.

const fs = require('fs');
const path = require('path');
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const autoprefixer = require('autoprefixer');
const postcssUrl = require('postcss-url');
const cssnano = require('cssnano');

const { NoEmitOnErrorsPlugin, SourceMapDevToolPlugin, NamedModulesPlugin } = require('webpack');
const { GlobCopyWebpackPlugin, BaseHrefWebpackPlugin } = require('@angular/cli/plugins/webpack');
const { CommonsChunkPlugin } = require('webpack').optimize;
const { AotPlugin } = require('@ngtools/webpack');

const nodeModules = path.join(process.cwd(), 'node_modules');
const realNodeModules = fs.realpathSync(nodeModules);
const genDirNodeModules = path.join(process.cwd(), 'src', '$$_gendir', 'node_modules');
const entryPoints = ["inline","polyfills","sw-register","styles","vendor","main"];
const minimizeCss = false;
const baseHref = "";
const deployUrl = "";
const postcssPlugins = function () {
        // safe settings based on: https://github.com/ben-eb/cssnano/issues/358#issuecomment-283696193
        const importantCommentRe = /@preserve|@license|[@#]\s*source(?:Mapping)?URL|^!/i;
        const minimizeOptions = {
            autoprefixer: false,
            safe: true,
            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.startsWith('/') || URL.startsWith('//')) {
                        return URL;
                    }
                    if (deployUrl.match(/:\/\//)) {
                        // If deployUrl contains a scheme, ignore baseHref use deployUrl as is.
                        return `${deployUrl.replace(/\/$/, '')}${URL}`;
                    }
                    else if (baseHref.match(/:\/\//)) {
                        // If baseHref contains a scheme, include it as is.
                        return baseHref.replace(/\/$/, '') +
                            `/${deployUrl}/${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}`.replace(/\/\/+/g, '/');
                    }
                }
            }),
            autoprefixer(),
        ].concat(minimizeCss ? [cssnano(minimizeOptions)] : []);
    };




module.exports = {
  "resolve": {
    "extensions": [
      ".ts",
      ".js"
    ],
    "modules": [
      "./node_modules",
      "./node_modules"
    ],
    "symlinks": true
  },
  "resolveLoader": {
    "modules": [
      "./node_modules",
      "./node_modules"
    ]
  },
  "entry": {
    "main": [
      "./src/main.ts"
    ],
    "polyfills": [
      "./src/polyfills.ts"
    ],
    "styles": [
      "./src/styles.css"
    ]
  },
  "output": {
    "path": path.join(process.cwd(), "dist"),
    "filename": "[name].bundle.js",
    "chunkFilename": "[id].chunk.js"
  },
  "module": {
    "rules": [
      {
        "enforce": "pre",
        "test": /\.js$/,
        "loader": "source-map-loader",
        "exclude": [
          /\/node_modules\//
        ]
      },
      {
        "test": /\.json$/,
        "loader": "json-loader"
      },
      {
        "test": /\.html$/,
        "loader": "raw-loader"
      },
      {
        "test": /\.(eot|svg)$/,
        "loader": "file-loader?name=[name].[hash:20].[ext]"
      },
      {
        "test": /\.(jpg|png|webp|gif|otf|ttf|woff|woff2|cur|ani)$/,
        "loader": "url-loader?name=[name].[hash:20].[ext]&limit=10000"
      },
      {
        "exclude": [
          path.join(process.cwd(), "src/styles.css")
        ],
        "test": /\.css$/,
        "use": [
          "exports-loader?module.exports.toString()",
          {
            "loader": "css-loader",
            "options": {
              "sourceMap": false,
              "importLoaders": 1
            }
          },
          {
            "loader": "postcss-loader",
            "options": {
              "ident": "postcss",
              "plugins": postcssPlugins
            }
          }
        ]
      },
      {
        "exclude": [
          path.join(process.cwd(), "src/styles.css")
        ],
        "test": /\.scss$|\.sass$/,
        "use": [
          "exports-loader?module.exports.toString()",
          {
            "loader": "css-loader",
            "options": {
              "sourceMap": false,
              "importLoaders": 1
            }
          },
          {
            "loader": "postcss-loader",
            "options": {
              "ident": "postcss",
              "plugins": postcssPlugins
            }
          },
          {
            "loader": "sass-loader",
            "options": {
              "sourceMap": false,
              "precision": 8,
              "includePaths": []
            }
          }
        ]
      },
      {
        "exclude": [
          path.join(process.cwd(), "src/styles.css")
        ],
        "test": /\.less$/,
        "use": [
          "exports-loader?module.exports.toString()",
          {
            "loader": "css-loader",
            "options": {
              "sourceMap": false,
              "importLoaders": 1
            }
          },
          {
            "loader": "postcss-loader",
            "options": {
              "ident": "postcss",
              "plugins": postcssPlugins
            }
          },
          {
            "loader": "less-loader",
            "options": {
              "sourceMap": false
            }
          }
        ]
      },
      {
        "exclude": [
          path.join(process.cwd(), "src/styles.css")
        ],
        "test": /\.styl$/,
        "use": [
          "exports-loader?module.exports.toString()",
          {
            "loader": "css-loader",
            "options": {
              "sourceMap": false,
              "importLoaders": 1
            }
          },
          {
            "loader": "postcss-loader",
            "options": {
              "ident": "postcss",
              "plugins": postcssPlugins
            }
          },
          {
            "loader": "stylus-loader",
            "options": {
              "sourceMap": false,
              "paths": []
            }
          }
        ]
      },
      {
        "include": [
          path.join(process.cwd(), "src/styles.css")
        ],
        "test": /\.css$/,
        "use": [
          "style-loader",
          {
            "loader": "css-loader",
            "options": {
              "sourceMap": false,
              "importLoaders": 1
            }
          },
          {
            "loader": "postcss-loader",
            "options": {
              "ident": "postcss",
              "plugins": postcssPlugins
            }
          }
        ]
      },
      {
        "include": [
          path.join(process.cwd(), "src/styles.css")
        ],
        "test": /\.scss$|\.sass$/,
        "use": [
          "style-loader",
          {
            "loader": "css-loader",
            "options": {
              "sourceMap": false,
              "importLoaders": 1
            }
          },
          {
            "loader": "postcss-loader",
            "options": {
              "ident": "postcss",
              "plugins": postcssPlugins
            }
          },
          {
            "loader": "sass-loader",
            "options": {
              "sourceMap": false,
              "precision": 8,
              "includePaths": []
            }
          }
        ]
      },
      {
        "include": [
          path.join(process.cwd(), "src/styles.css")
        ],
        "test": /\.less$/,
        "use": [
          "style-loader",
          {
            "loader": "css-loader",
            "options": {
              "sourceMap": false,
              "importLoaders": 1
            }
          },
          {
            "loader": "postcss-loader",
            "options": {
              "ident": "postcss",
              "plugins": postcssPlugins
            }
          },
          {
            "loader": "less-loader",
            "options": {
              "sourceMap": false
            }
          }
        ]
      },
      {
        "include": [
          path.join(process.cwd(), "src/styles.css")
        ],
        "test": /\.styl$/,
        "use": [
          "style-loader",
          {
            "loader": "css-loader",
            "options": {
              "sourceMap": false,
              "importLoaders": 1
            }
          },
          {
            "loader": "postcss-loader",
            "options": {
              "ident": "postcss",
              "plugins": postcssPlugins
            }
          },
          {
            "loader": "stylus-loader",
            "options": {
              "sourceMap": false,
              "paths": []
            }
          }
        ]
      },
      {
        "test": /\.ts$/,
        "loader": "@ngtools/webpack"
      }
    ]
  },
  "plugins": [
    new NoEmitOnErrorsPlugin(),
    new GlobCopyWebpackPlugin({
      "patterns": [
        "assets",
        "favicon.ico"
      ],
      "globOptions": {
        "cwd": path.join(process.cwd(), "src"),
        "dot": true,
        "ignore": "**/.gitkeep"
      }
    }),
    new ProgressPlugin(),
    new SourceMapDevToolPlugin({
      "filename": "[file].map[query]",
      "moduleFilenameTemplate": "[resource-path]",
      "fallbackModuleFilenameTemplate": "[resource-path]?[hash]",
      "sourceRoot": "webpack:///"
    }),
    new HtmlWebpackPlugin({
      "template": "./src/index.html",
      "filename": "./index.html",
      "hash": false,
      "inject": true,
      "compile": true,
      "favicon": false,
      "minify": false,
      "cache": true,
      "showErrors": true,
      "chunks": "all",
      "excludeChunks": [],
      "title": "Webpack App",
      "xhtml": true,
      "chunksSortMode": function sort(left, right) {
        let leftIndex = entryPoints.indexOf(left.names[0]);
        let rightindex = entryPoints.indexOf(right.names[0]);
        if (leftIndex > rightindex) {
            return 1;
        }
        else if (leftIndex < rightindex) {
            return -1;
        }
        else {
            return 0;
        }
    }
    }),
    new BaseHrefWebpackPlugin({}),
    new CommonsChunkPlugin({
      "minChunks": 2,
      "async": "common"
    }),
    new CommonsChunkPlugin({
      "name": [
        "inline"
      ],
      "minChunks": null
    }),
    new CommonsChunkPlugin({
      "name": [
        "vendor"
      ],
      "minChunks": (module) => {
                return module.resource
                    && (module.resource.startsWith(nodeModules)
                        || module.resource.startsWith(genDirNodeModules)
                        || module.resource.startsWith(realNodeModules));
            },
      "chunks": [
        "main"
      ]
    }),
    new NamedModulesPlugin({}),
    new AotPlugin({
      "mainPath": "main.ts",
      "hostReplacementPaths": {
        "environments/environment.ts": "environments/environment.ts"
      },
      "exclude": [],
      "tsConfigPath": "src/tsconfig.app.json",
      "skipCodeGeneration": true
    })
  ],
  "node": {
    "fs": "empty",
    "global": true,
    "crypto": "empty",
    "tls": "empty",
    "net": "empty",
    "process": true,
    "module": false,
    "clearImmediate": false,
    "setImmediate": false
  },
  "devServer": {
    "historyApiFallback": true
  }
};

Coffee2CodeNL avatar Jul 29 '17 11:07 Coffee2CodeNL

@weaverryan is this useful?

I've also been publishing a link to this issue on various platforms to see if people can help out.

Coffee2CodeNL avatar Jul 30 '17 11:07 Coffee2CodeNL

@stof do you perhaps have a clue for us? :smile:

Coffee2CodeNL avatar Aug 01 '17 19:08 Coffee2CodeNL

This does help. I actually don't see anything special in this setup, except for enabling typescript and also the AotPlugin.

@iSDP It might already be possible to use Encore with Angular just by calling enableTypeScriptLoader(). What I really need is someone to help out and do some of the work for me :). I don't use Angular, but would like to support it. Could you try using Angular and Encore by just calling enableTypeScriptLoader()? And also by trying to use the AotPlugin (you can add it via addPlugin()).

Thanks!

weaverryan avatar Aug 05 '17 11:08 weaverryan

Hi and thanks for your answers. Actually I ended up using React. As I just landed on the symfony project, I've been given the goal to implement a reactive solution. But Angular appeared to be a bad choice because of its monolithique aspect. Developing and operating à progressive migration of the interface was more suited by react or vuejs. But what you Shared @iSDP is still à huge help, thanks. And thanks to you @weaverryan and the team working on Encore. I would be glad to help if I had the time to...

neovea avatar Aug 24 '17 17:08 neovea

Hi,

@weaverryan It's working out of the box as you already guessed.

But Angular 2+ is not that big benefit for a Symfony application, because it's more useful for single page applications then for mixing with twig. I will give vue.js a try instead. I hope getting Angular 5 running using Webpack Encore will help somebody, anyway.

Angular 5 (Angular 2-4 will need another plugin (AotPlugin) to be injected) can be added like this...

npm install --save-dev @ngtools/webpack

// webpack.config.js
var Encore = require('@symfony/webpack-encore');
var AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;

Encore
.enableTypeScriptLoader()
.addPlugin(new AngularCompilerPlugin({
        tsConfigPath: './PathTo/tsconfig.json',
        entryModule: './PathTo/AngularBootstrapModule'
    }))
;

var config = Encore.getWebpackConfig();

config.resolve = {
    extensions: ['.ts']
};

module.exports = config;

D37R4C7 avatar Nov 07 '17 14:11 D37R4C7

Holy reproducing feces!

This would help a ton as I won't have to switch projects to work on the angular frontend and a Symfony based API as backend at the same time 👌

Coffee2CodeNL avatar Nov 07 '17 14:11 Coffee2CodeNL

I have managed to put Angular5 app into Symfony 3 project with 2 step compiling. Here is what I have done (it was some time ago so I may have missed sth):

  1. generate new angular project outside the Symfony
  2. put angular src content into symfony src/AngularApp
  3. put angular .angular-cli.json, tslint.json into symfony root folder
  4. put angular's tsconfig.json to symfony's src dir
  5. add missing dependencies from angular's package.json to symfony's package.json
  6. configure .angular-cli.json: "apps": [{ root: "src/AngularApp", "outDir": "assets/angularApp" .... }]
  7. add scripts from angular's package.json to symfony's package.json (some name changes required)
  8. in package.json my dev build command is now "ng build && ./node_modules/.bin/encore dev"
  9. in webpack.config.js you need to add entries for ./assets/angularApp/polyfills.bundle.js, ./assets/angularApp/styles.bundle.js, ./assets/angularApp/vendor.bundle.js, ./assets/angularApp/main.bundle.js and add them in your twig in the same order
  10. put in your twig and you should see angular app working

you can also use ng serve to develop but you will not have your twig template

mateusz-sawit avatar Jan 03 '18 13:01 mateusz-sawit

@mateusz-sawit that worked really well.

Also, something you may have forgotten (at least required for me), was to also add the generated inline.bundle.js to the webpack.config.js and add it first (among the other scripts) in the twig.

rottenoats avatar Feb 13 '18 12:02 rottenoats

@Rebolon ng serve uses WebpackDevServer internally : https://github.com/angular/angular-cli/blob/master/packages/%40angular/cli/tasks/serve.ts

You can probably achieve the same thing by running yarn encore dev-server.

Lyrkan avatar Mar 13 '18 08:03 Lyrkan

@Lyrkan maybe we would have to eject the webpack config from angular-cli ? i'm trying to use watch options on ng build. I'll post my resulst here

(i delete my post by mistake... instead of editing it: so what i was saying was (summarised): with previous post we don't have a watch system)

Rebolon avatar Mar 13 '18 08:03 Rebolon

@Rebolon you can use ng serve in my configuration - in package.json add script: "serve": "ng serve" and then run npm serve - you will have your angular app running alone but without "symfony" content

moreover you should be able to use any ng commands in your project - i.e. ng generate, and the generated content should be put in right place.

mateusz-sawit avatar Mar 13 '18 09:03 mateusz-sawit

For instance i decided to use only ng build --watch command it will generate assets files in my public/dist folder that can be included in a template.

This is for a ProofOfConcept so i think it's enough. The bad thing is that even if it rebuild files when there is a changes, it doesn't refressh the browser. I have to setup something manually but i'm not sure it's the best idea. I think in fact that if i need angular it must be a standalone application whereas if i need to embed JS in PHP app, then it's more reliable to use React or VueJS

Rebolon avatar Mar 13 '18 13:03 Rebolon

@mateusz-sawit Your idea seems good but your files are being webpacked by angular CLI, not by encore, right? I see a problem here: if you want to uglify the files you can't use encore for it, you should use angular CLI so in Encore config you don't now the name for each addEntry().

How did you solve it? Are you uglifying the JS files? if so are you doing it from Encore? (I think ng build prod makes more things than just uglifying)

senechaux avatar Apr 02 '18 11:04 senechaux

Hi, I have added bash script to remove hash part of ng generated files. I run this between ng build and encore

mateusz-sawit avatar Apr 02 '18 14:04 mateusz-sawit

@mateusz-sawit Thanks for your quick response, we are going to use a bash script to generate the manifest.json avoiding Encore.

senechaux avatar Apr 02 '18 14:04 senechaux

BTW @mateusz-sawit you can use ng build --prod --output-hashing=none to avoid name hashing.

senechaux avatar Apr 02 '18 15:04 senechaux

Hey @mateusz-sawit thankx for your gr8 post abt putting angular into a symfony app. Do u have a working example you can share? How did manage routing btw angular and symfony? @senechaux if you have an example too, plz don’t hesitate to share

vichanse avatar Sep 06 '19 20:09 vichanse

Hey @vichanse for now I don't have code that I can share but in my working app I have modified a little bit way I am doing it. Because putting angular build results into webpack required compiling angular files twice I use separate builds for angular and symfonny assets.

  1. Because my app is called "calculator" i have created symlink from angular's dist path to /web/build/calculator

  2. I have created symfony command that fills generated manifest.json with angular app:

     protected function execute(InputInterface $input, OutputInterface $output)
     {
         $this->output = $output;
         $project_dir = $this->getContainer()->getParameter('kernel.project_dir');
         $manifestPath = $project_dir . '/web/build/manifest.json';
         $angularPath = $project_dir . '/web/build/calculator';
    
         if(!file_exists($manifestPath)) {
             $this->output->writeln(
                 '<error>No manifest file. Run encore first!</error>'
             );
             return;
         }
         if(!is_dir($angularPath)) {
             $this->output->writeln(
                 '<error>No calculator app. Run ng build first!</error>'
             );
             return;
         }
    
         $manifestContent = file_get_contents($manifestPath);
         $manifest = json_decode($manifestContent);
    
         foreach (scandir($angularPath) as $file) {
             if(strpos($file, '.js')) {
                 switch (substr($file, 0, 4)) {
                     case 'inli':
                     case 'runt':
                         $manifest->{'build/calculator/inline.js'} = '/build/calculator/' . $file; break;
                     case 'main':
                         $manifest->{'build/calculator/app.js'} = '/build/calculator/' . $file; break;
                     case 'poly':
                         $manifest->{'build/calculator/polyfills.js'} = '/build/calculator/' . $file; break;
                     case 'scri':
                         $manifest->{'build/calculator/vendor.js'} = '/build/calculator/' . $file; break;
                 }
             }
             elseif(strpos($file, '.css') && substr($file, 0, 6) == 'styles') {
                 $manifest->{'build/calculator/styles.css'} = '/build/calculator/' . $file;
             }
         }
    
         file_put_contents($manifestPath, json_encode($manifest, JSON_UNESCAPED_SLASHES));
     }
    
  3. To build both apps you need to run encore prod && ng build --prod && php bin/console yourManifestUpdateCommand

You don't need to add anything to symfony's webpack.config.js

  1. To include app in twig you simply put:

     <script src="{{ asset('build/calculator/inline.js') }}"></script>
     <script src="{{ asset('build/calculator/polyfills.js') }}"></script>
     <script src="{{ asset('build/calculator/vendor.js') }}"></script>
     <script src="{{ asset('build/calculator/app.js') }}"></script>
    
  2. About routing my angular app is in a subroute so I have configured symfony route with wildcard pattern for subroutes to always return template with angular app

mateusz-sawit avatar Sep 16 '19 09:09 mateusz-sawit

I have managed to put Angular5 app into Symfony 3 project with 2 step compiling. Here is what I have done (it was some time ago so I may have missed sth):

  1. generate new angular project outside the Symfony
  2. put angular src content into symfony src/AngularApp
  3. put angular .angular-cli.json, tslint.json into symfony root folder
  4. put angular's tsconfig.json to symfony's src dir
  5. add missing dependencies from angular's package.json to symfony's package.json
  6. configure .angular-cli.json: "apps": [{ root: "src/AngularApp", "outDir": "assets/angularApp" .... }]
  7. add scripts from angular's package.json to symfony's package.json (some name changes required)
  8. in package.json my dev build command is now "ng build && ./node_modules/.bin/encore dev"
  9. in webpack.config.js you need to add entries for ./assets/angularApp/polyfills.bundle.js, ./assets/angularApp/styles.bundle.js, ./assets/angularApp/vendor.bundle.js, ./assets/angularApp/main.bundle.js and add them in your twig in the same order
  10. put in your twig and you should see angular app working

you can also use ng serve to develop but you will not have your twig template

@mateusz-sawit Hello. I added angular to my symfony application but I have some problems. When I tried encore dev I take such message:

 - webpack-cli (https://github.com/webpack/webpack-cli)
   The original webpack full-featured CLI.
We will use "yarn" to install the CLI via "yarn add -D".
Do you want to install 'webpack-cli' (yes/no): 
  • When I select no build is failing
  • When I select yes after installing webpack-cli, in console:

(node:28369) DeprecationWarning: Tapable.plugin is deprecated. Use new API on .hooks instead /Users/user/Documents/work/project/node_modules/webpack/lib/Chunk.js:835 throw new Error("Chunk.parents: Use ChunkGroup.getParents() instead");

Maybe You had the same problem?

andrewchaika avatar Oct 24 '19 17:10 andrewchaika

@andrewchaika Hi, I am not using encore to compile Symfony app, see my comment on 16 Sep. I am compiling ng app separately, copying/linking generated sources to /web/build dir and manually or with script updating manifest.json file. It solved many problems with compiling angular with encore.

mateusz-sawit avatar Nov 04 '19 19:11 mateusz-sawit

Coming back to this , any updates on how i should procede

AlaaCherif avatar Aug 11 '22 16:08 AlaaCherif