nippur72 / ifdef-loader

Webpack loader for JavaScript/TypeScript conditional compilation.
286 stars 28 forks source link

@ngtools/webpack #4

Open KhoaSydney opened 7 years ago

KhoaSydney commented 7 years ago

Hi,

Instead of ts-loader, I have use ngtools/webpack which compile and perform AoT. does ifdef-loader work with ngtools/webpack? cheers, Khoa

nippur72 commented 7 years ago

I have not tested it with ngtools/webpack but I guess it should work, you only need to chain it properly, e.g.:

rules: [
      {
        test: /\.ts$/,
        loader: ['@ngtools/webpack', 'ifdef-loader?${q}']
      }
    ]
KhoaSydney commented 7 years ago

hi @nippur72 ,

It looks like it is working with ngtools/webpack. I run across a strange issue with lazy loaded module. ngtools/webpack compile typescripts with AoT (Angular). For AoT to work, codes should be statically analyzable. Will the code below able to pick up by ngtool/webpack?

@NgModule({
    imports: [
        CommonModule,
        BrowserModule,
        BrowserAnimationsModule,
        FormsModule,
        HttpModule,
        ControlsModule,
        HomeModule,
        AdvancedAdminModule,
        ClientModule,
        SettingsModule,
        MidwinterModule,
        ReportingModule,
        ModellingModule,
        PracticeManagementModule,
        UserManagementModule,
        PortfolioBookNg2Module,
        /// #if PRODUCTION
        RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),
        /// #else
        RouterModule.forRoot(routes),
        /// #endif
        UpgradeModule

    ],

How does your loader work in relation to ngtools/webpack? does it feed the codes (let say this is production) into ngtools/webpack like below?


@NgModule({
    imports: [
        CommonModule,
        BrowserModule,
        BrowserAnimationsModule,
        FormsModule,
        HttpModule,
        ControlsModule,
        HomeModule,
        AdvancedAdminModule,
        ClientModule,
        SettingsModule,
        MidwinterModule,
        ReportingModule,
        ModellingModule,
        PracticeManagementModule,
        UserManagementModule,
        PortfolioBookNg2Module,

        RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),

        UpgradeModule

    ]```
nippur72 commented 7 years ago

yes it will feed the code with

RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),

when the flag PRODUCTION is on.

Going a bit off-topic, if your scenario is a simple PRODUCTION/DEVELOPMENT environment you might not need ifdef-loader altogether. Just use simple if expressions and when building for PRODUCTION let uglify-js (in webpack) strip out the debug code for you. It's much cleaner and fits better with TypeScript syntax (if you use it).

Example:

serManagementModule,
PortfolioBookNg2Module,
PRODUCTION ? RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })  : RouterModule.forRoot(routes),
UpgradeModule  

and then use this plugin in webpack:

   const uglify = new webpack.optimize.UglifyJsPlugin({ 
      compress: { 
         warnings: false,
         global_defs: { 
            PRODUCTION: true /* or false */,
         }
      }
   });
KhoaSydney commented 7 years ago

hI @nippur72 ,

It will not work for AoT build though. I get the following error: ERROR in Error encountered resolving symbol values statically. Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler (position 46:20 in the original .ts file), resolving symbol PRODUCTION.

nippur72 commented 7 years ago

I am not sure, but if you are using TypeScript maybe you need to declare the PRODUCTION and DEVELOPMENT variables that are externally injected by uglify-js:

// somewhere in a .ts
declare global {
   export var PRODUCTION;
   export var DEVELOPMENT;
} 

Also, you need to manually set the above two variables when not using uglify-js (that is, during DEVELOPMENT):

if(PRODUCTION === undefined) {
  window["PRODUCTION"] = false;
  window["DEVELOPMENT"] = true;
}
// note that the above code will erased in production
stephenlautier commented 6 years ago

This was working for me for @ngtools/webpack, however after the last update of 1.8.0+ e.g. AngularCompilerPlugin instead of AoTPlugin it stopped working.

Tried to update this lib to 2.x cause I was still on 1.x and its not working either.

This is how im doing it now

const ifDefOpts = querystring.encode({
    DEV: isDevBuild,
});

test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, use: isDevBuild
    ? ["awesome-typescript-loader?silent=true", "angular2-template-loader", `ifdef-loader?${ifDefOpts}`]
    : ["@ngtools/webpack", `ifdef-loader?${ifDefOpts}`]
nippur72 commented 6 years ago

@stephenlautier you can use the echo-loader to debug exactly what is happening in the chain of your loaders.

For instance you can put it after ifdef-loader and see if it is actually eliminating code:

test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, use: isDevBuild
    ? ["awesome-typescript-loader?silent=true", "angular2-template-loader", "echo-loader?msg=dump", `ifdef-loader?${ifDefOpts}`]
    : ["@ngtools/webpack", "echo-loader?msg=dump", `ifdef-loader?${ifDefOpts}`]

(The dump option will cause the file to be dumped on the console)

stephenlautier commented 6 years ago

@nippur72 thanks for the info! im not a webpack guru so it definitely helps 👍

i will try and give that a shot

stephenlautier commented 6 years ago

hmm.. its very strange, i tested out with @nippur72 and the code is not there, so not sure how its giving an error 😕

source

import "reflect-metadata";
import "zone.js";
import "hammerjs";
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

// todo: get this back working
/// #if DEV
// import "./app/app-light.theme.scss";
import "./app/app-dark.theme.scss";
/// #endif

import { AppModule } from "./app/app.module.browser";

dump after

dump: boot.browser.ts
*************
import "reflect-metadata";
import "zone.js";
import "hammerjs";
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

// todo: get this back working
///////////
///////////////////////////////////////
///////////////////////////////////
//////////

import { AppModule } from "./app/app.module.browser";

so it is actually getting removed, not sure then why/how its blowing and from where its resolving it, this is the error

  ERROR in ./ClientApp/app/app-dark.theme.scss
    Module parse failed: Unexpected character '@' (1:0)
    You may need an appropriate loader to handle this file type.
    | @import "themes/dark.theme";
    | @import "settings";
     @ ./ClientApp/boot.browser.ts 8:0-35

and if i comment out manually the scss import, it works

nippur72 commented 6 years ago

For some reason webpack is picking the unprocessed .ts file, perhaps with another loader. Can you share your webpack.config.js ?

stephenlautier commented 6 years ago

Sure, this is all of it, and it happens when run prod mode, so isDevBuild if false

const path = require("path");
const fs = require("fs");
const querystring = require("querystring");
const chalk = require("chalk");
const webpack = require("webpack");
const merge = require("webpack-merge");
const AngularCompilerPlugin = require("@ngtools/webpack").AngularCompilerPlugin;
const CheckerPlugin = require("awesome-typescript-loader").CheckerPlugin;
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = (env) => {
    const isDevBuild = !(env && env.prod);
    const srcRoot = "ClientApp";
    console.log(chalk`{cyan.bold Build environment isDev: ${isDevBuild}}`);
    const ifDefOpts = querystring.encode({
        DEV: isDevBuild,
    });
    const extractSass = new ExtractTextPlugin({
        filename: "[name].css",
        disable: isDevBuild
    });
    const sassConfig = extractSass.extract({
        use: [{
            loader: "css-loader", // translates CSS into CommonJS
            options: {
                minimize: !isDevBuild
            }
        }, {
            loader: "sass-loader", // compiles Sass to CSS
            options: {
                includePaths: [
                    `./${srcRoot}/assets/styles`,
                    `./${srcRoot}/libraries`
                ]
            }
        }],
        // use style-loader in development
        fallback: "style-loader" // creates style nodes from JS strings
    });

    // Configuration in common to both client-side and server-side bundles
    const sharedConfig = {
        stats: { modules: false },
        context: __dirname,
        resolve: { extensions: [".js", ".ts"] },
        output: {
            filename: "[name].js",
            publicPath: "dist/" // Webpack dev middleware, if enabled, handles requests for this URL prefix
        },
        module: {
            rules: [
                {
                    test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, use: isDevBuild
                        ? ["awesome-typescript-loader?silent=true", "angular2-template-loader", `ifdef-loader?${ifDefOpts}`]
                        : ["@ngtools/webpack", `ifdef-loader?${ifDefOpts}`]
                },
                { test: /\.html$/, use: "html-loader?minimize=false" },
                { test: /\.css$/, use: ["to-string-loader", isDevBuild ? "css-loader" : "css-loader?minimize"] },
                { test: /\.(png|jpg|jpeg|gif|svg)$/, use: "url-loader?limit=1000000" }
            ].concat(isDevBuild ? [
                // Plugins that apply in development builds only
                { test: /\.scss$/, use: sassConfig },
                { test: /\.js$/, use: ["source-map-loader"], enforce: "pre" }
            ] : [])
        },
        plugins: [
            new webpack.DefinePlugin({
                "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || isDevBuild ? "development" : "production")
            }),
            new CheckerPlugin(),
            extractSass
        ]
    };

    // Configuration for client-side bundle suitable for running in browsers
    const clientBundleOutputDir = "./wwwroot/dist";
    const clientBundleConfig = merge(sharedConfig, {
        entry: { "main-client": `./${srcRoot}/boot.browser.ts` },
        output: { path: path.join(__dirname, clientBundleOutputDir) },
        plugins: [
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require("./wwwroot/dist/vendor-manifest.json")
            })
        ].concat(isDevBuild ? [
            // Plugins that apply in development builds only
            new webpack.SourceMapDevToolPlugin({
                filename: "[file].map", // Remove this line if you prefer inline source maps
                moduleFilenameTemplate: path.relative(clientBundleOutputDir, "[resourcePath]") // Point sourcemap entries to the original file locations on disk
            })
        ] : [
                // Plugins that apply in production builds only
                new webpack.optimize.UglifyJsPlugin(),
                new AngularCompilerPlugin({
                    tsConfigPath: "./tsconfig.browser.json",
                    entryModule: path.join(__dirname, "ClientApp/app/app.module.browser#AppModule"),
                    compilerOptions: {
                        noUnusedParameters: false,
                    },
                })
            ])
    });

    let sassBundleConfig;
    if (!isDevBuild) {
        const themes = getThemes(`./${srcRoot}/app`);
        sassBundleConfig = {
            stats: { modules: false },
            context: __dirname,
            output: {
                filename: "[name].css",
                path: path.join(__dirname, clientBundleOutputDir),
                publicPath: "/dist/" // Webpack dev middleware, if enabled, handles requests for this URL prefix
            },
            entry: themes,
            module: {
                rules: [
                    { test: /\.scss$/, use: sassConfig }
                ]
            },
            plugins: [extractSass]
        };
    }

    // Configuration for server-side (prerendering) bundle suitable for running in Node
    let serverBundleConfig = merge(sharedConfig, {
        entry: { "main-server": `./${srcRoot}/boot.server.ts` },
        plugins: [
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require(`./${srcRoot}/dist/vendor-manifest.json`),
                sourceType: "commonjs2",
                name: "./vendor"
            })
        ].concat(isDevBuild ? [] : [
            // Plugins that apply in production builds only
            new AngularCompilerPlugin({
                tsConfigPath: "./tsconfig.server.json",
                entryModule: path.join(__dirname, "ClientApp/app/app.module.server#AppModule"),
                compilerOptions: {
                    noUnusedParameters: false,
                }
            })
        ]),
        output: {
            libraryTarget: "commonjs",
            path: path.join(__dirname, `./${srcRoot}/dist`)
        },
        target: "node",
        devtool: "inline-source-map"
    });

    if (isDevBuild) {
        // todo: try to change back to "main" only, as it reduces file size drastically - with it @odin pkgs are being loaded twice duplicated (umd/esm).
        serverBundleConfig = merge(serverBundleConfig, {
            resolve: { mainFields: ["main"] },
        });
    }

    return [
        clientBundleConfig,
        serverBundleConfig,
        sassBundleConfig,
    ].filter(x => x);
};

// todo: move to build-tools
function getThemes(themePath) {
    let themes = {};
    fs.readdirSync(themePath).forEach(fileName => {
        const fileNameWithPath = path.resolve(themePath, fileName);
        const stat = fs.lstatSync(fileNameWithPath);
        if (stat.isDirectory()) return;
        if (!/\.theme\.scss$/.test(fileName)) return;

        const nameWithoutExt = path.basename(fileName, ".scss");
        themes[nameWithoutExt] = fileNameWithPath;
    });

    return themes;
};
nippur72 commented 6 years ago

sorry I was not able to locate the issue, I tried to replicate your env but got lost along the way 😞

If the problem is in any of the loaders you can debug which one is actually being triggered, again with the echo-loader, e.g:

{ test: /\.html$/, use: ["html-loader?minimize=false","echo-loader?msg=html_loader"] },

With this method I once discovered a file being picked twice.

stephenlautier commented 6 years ago

thanks for trying! if i get some time I have a simpler project which is similar because that's based on a template, I will update it and get it with the same issue, and you can try there if you'd like

mamacdon commented 6 years ago

According to this bug, @ngtools/webpack is cheating: https://github.com/angular/angular-cli/issues/8870. It reads input files from disk instead of receiving them through webpack's loader chain, which breaks pre-processing.

I had some luck reversing the order: i.e. running ifdef-loader on the output of @ngtools/webpack rather than the input to it. This appears to work since @ngtools/webpack preserves comments. Here's an example.


Input file:

let AuthServiceProvider: Provider = AuthService;

/// #if MODE === 'mock'
console.log('using mock AuthService');
AuthServiceProvider = require('./auth.service.mock').MockAuthServiceProvider;
/// #endif

webpack config:

{ 
  rules: [ {
      test: /\.ts$/,
      use: [
          { loader: 'echo-loader?msg=dump' }, // runs last
          { loader: '@ngtools/webpack' },
          { loader: 'ifdef-loader' },         // runs first
      ]
  }],
}

Output:

var AuthServiceProvider = AuthService;
/// #if MODE === 'mock'             <--- WRONG: the if block was not evaluated
console.log('using mock AuthService');
AuthServiceProvider = require('./auth.service.mock').MockAuthServiceProvider;
/// #endif

When I reverse the order of @ngtools/webpack and ifdef-loader in my webpack config, I see the condition gets evaluated:

false

var AuthServiceProvider = AuthService;
///////////////////////
//////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////
//////////

true

var AuthServiceProvider = AuthService;
///////////////////////
console.log('using mock AuthService');
AuthServiceProvider = require('./auth.service.mock').MockAuthServiceProvider;
//////////

But this approach is fragile since it relies on an implementation detail of @ngtools/webpack.

stephenlautier commented 6 years ago

@mamacdon thanks i managed to get this working.

Just another issue we encountered is the following

This wasnt working

/// #if DEV
import "./app/app-dark.theme.scss";
/// #endif

import { AppModule } from "./app/app.module.browser";

This works

import { AppModule } from "./app/app.module.browser";
/// #if DEV
import "./app/app-dark.theme.scss";
/// #endif

For some reason the first one was stripping the /// #endif