"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const path = require("path");
const webpack = require("webpack");
const chalk_1 = require("chalk");
const app_utils_1 = require("../utilities/app-utils");
const webpack_config_1 = require("../models/webpack-config");
const config_1 = require("../models/config");
const strip_bom_1 = require("../utilities/strip-bom");
const webpack_1 = require("@ngtools/webpack");
const build_optimizer_1 = require("@angular-devkit/build-optimizer");
const license_webpack_plugin_1 = require("license-webpack-plugin");
const denodeify = require("denodeify");
const common_tags_1 = require("common-tags");
const exists = (p) => Promise.resolve(fs.existsSync(p));
const writeFile = denodeify(fs.writeFile);
const angularCliPlugins = require('../plugins/webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const SubresourceIntegrityPlugin = require('webpack-subresource-integrity');
const SilentError = require('silent-error');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const ConcatPlugin = require('webpack-concat-plugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const Task = require('../ember-cli/lib/models/task');
const ProgressPlugin = require('webpack/lib/ProgressPlugin');
exports.pluginArgs = Symbol('plugin-args');
exports.postcssArgs = Symbol('postcss-args');
const yellow = chalk_1.default.yellow;
const pree2eNpmScript = `webdriver-manager update --standalone false --gecko false --quiet`;
class JsonWebpackSerializer {
    constructor(_root, _dist, _appRoot) {
        this._root = _root;
        this._dist = _dist;
        this._appRoot = _appRoot;
        this.imports = {};
        this.variableImports = {
            'fs': 'fs',
            'path': 'path',
        };
        this.variables = {
            'nodeModules': `path.join(process.cwd(), 'node_modules')`,
            'realNodeModules': `fs.realpathSync(nodeModules)`,
            'genDirNodeModules': `path.join(process.cwd(), '${this._appRoot}', '$$_gendir', 'node_modules')`,
        };
        this._postcssProcessed = false;
    }
    _escape(str) {
        return '\uFF01' + str + '\uFF01';
    }
    _serializeRegExp(re) {
        return this._escape(re.toString());
    }
    _serializeFunction(fn) {
        return this._escape(fn.toString());
    }
    _relativePath(of, to) {
        return this._escape(`path.join(${of}, ${JSON.stringify(to)})`);
    }
    _addImport(module, importName) {
        if (!this.imports[module]) {
            this.imports[module] = [];
        }
        if (this.imports[module].indexOf(importName) == -1) {
            this.imports[module].push(importName);
        }
    }
    _globCopyWebpackPluginSerialize(value) {
        let patterns = value.options.patterns;
        let globOptions = value.options.globOptions;
        return {
            patterns,
            globOptions: this._globReplacer(globOptions)
        };
    }
    _insertConcatAssetsWebpackPluginSerialize(value) {
        return value.entryNames;
    }
    _commonsChunkPluginSerialize(value) {
        let minChunks = value.minChunks;
        switch (typeof minChunks) {
            case 'function':
                minChunks = this._serializeFunction(value.minChunks);
                break;
        }
        return {
            name: value.chunkNames,
            filename: value.filenameTemplate,
            minChunks,
            chunks: value.selectedChunks,
            async: value.async,
            minSize: value.minSize
        };
    }
    _extractTextPluginSerialize(value) {
        return {
            filename: value.filename,
            disable: value.options.disable
        };
    }
    _aotPluginSerialize(value) {
        const tsConfigPath = path.relative(this._root, value.options.tsConfigPath);
        const basePath = path.dirname(tsConfigPath);
        return Object.assign({}, value.options, {
            tsConfigPath,
            mainPath: path.relative(basePath, value.options.mainPath),
            hostReplacementPaths: Object.keys(value.options.hostReplacementPaths)
                .reduce((acc, key) => {
                const replacementPath = value.options.hostReplacementPaths[key];
                key = path.relative(basePath, key);
                acc[key] = path.relative(basePath, replacementPath);
                return acc;
            }, {}),
            exclude: Array.isArray(value.options.exclude)
                ? value.options.exclude.map((p) => {
                    return p.startsWith('/') ? path.relative(basePath, p) : p;
                })
                : value.options.exclude
        });
    }
    _htmlWebpackPlugin(value) {
        const chunksSortMode = value.options.chunksSortMode;
        this.variables['entryPoints'] = JSON.stringify(chunksSortMode.entryPoints);
        return Object.assign({}, value.options, {
            template: './' + path.relative(this._root, value.options.template),
            filename: './' + path.relative(this._dist, value.options.filename),
            chunksSortMode: this._serializeFunction(chunksSortMode)
        });
    }
    _environmentPlugin(plugin) {
        return plugin.defaultValues;
    }
    _licenseWebpackPlugin(plugin) {
        return plugin.options;
    }
    _concatPlugin(plugin) {
        const options = plugin.settings;
        if (!options || !options.filesToConcat) {
            return options;
        }
        const filesToConcat = options.filesToConcat
            .map((file) => path.relative(process.cwd(), file));
        return Object.assign({}, options, { filesToConcat });
    }
    _uglifyjsPlugin(plugin) {
        return plugin.options;
    }
    _pluginsReplacer(plugins) {
        return plugins.map(plugin => {
            let args = plugin.options || undefined;
            const serializer = (args) => JSON.stringify(args, (k, v) => this._replacer(k, v), 2);
            switch (plugin.constructor) {
                case ProgressPlugin:
                    this.variableImports['webpack/lib/ProgressPlugin'] = 'ProgressPlugin';
                    break;
                case webpack.NoEmitOnErrorsPlugin:
                    this._addImport('webpack', 'NoEmitOnErrorsPlugin');
                    break;
                case webpack.NamedModulesPlugin:
                    this._addImport('webpack', 'NamedModulesPlugin');
                    break;
                case webpack.HashedModuleIdsPlugin:
                    this._addImport('webpack', 'HashedModuleIdsPlugin');
                    break;
                case webpack.SourceMapDevToolPlugin:
                    this._addImport('webpack', 'SourceMapDevToolPlugin');
                    break;
                case webpack.optimize.UglifyJsPlugin:
                    this._addImport('webpack.optimize', 'UglifyJsPlugin');
                    break;
                case webpack.optimize.ModuleConcatenationPlugin:
                    this._addImport('webpack.optimize', 'ModuleConcatenationPlugin');
                    break;
                case angularCliPlugins.BaseHrefWebpackPlugin:
                case angularCliPlugins.NamedLazyChunksWebpackPlugin:
                case angularCliPlugins.SuppressExtractedTextChunksWebpackPlugin:
                    this._addImport('@angular/cli/plugins/webpack', plugin.constructor.name);
                    break;
                case angularCliPlugins.GlobCopyWebpackPlugin:
                    args = this._globCopyWebpackPluginSerialize(plugin);
                    this._addImport('@angular/cli/plugins/webpack', 'GlobCopyWebpackPlugin');
                    break;
                case angularCliPlugins.InsertConcatAssetsWebpackPlugin:
                    args = this._insertConcatAssetsWebpackPluginSerialize(plugin);
                    this._addImport('@angular/cli/plugins/webpack', 'InsertConcatAssetsWebpackPlugin');
                    break;
                case webpack.optimize.CommonsChunkPlugin:
                    args = this._commonsChunkPluginSerialize(plugin);
                    this._addImport('webpack.optimize', 'CommonsChunkPlugin');
                    break;
                case ExtractTextPlugin:
                    args = this._extractTextPluginSerialize(plugin);
                    this.variableImports['extract-text-webpack-plugin'] = 'ExtractTextPlugin';
                    break;
                case CircularDependencyPlugin:
                    this.variableImports['circular-dependency-plugin'] = 'CircularDependencyPlugin';
                    break;
                case webpack_1.AotPlugin:
                    args = this._aotPluginSerialize(plugin);
                    this._addImport('@ngtools/webpack', 'AotPlugin');
                    break;
                case build_optimizer_1.PurifyPlugin:
                    this._addImport('@angular-devkit/build-optimizer', 'PurifyPlugin');
                    break;
                case webpack_1.AngularCompilerPlugin:
                    args = this._aotPluginSerialize(plugin);
                    this._addImport('@ngtools/webpack', 'AngularCompilerPlugin');
                    break;
                case HtmlWebpackPlugin:
                    args = this._htmlWebpackPlugin(plugin);
                    this.variableImports['html-webpack-plugin'] = 'HtmlWebpackPlugin';
                    break;
                case webpack.EnvironmentPlugin:
                    args = this._environmentPlugin(plugin);
                    this._addImport('webpack', 'EnvironmentPlugin');
                    break;
                case license_webpack_plugin_1.LicenseWebpackPlugin:
                    args = this._licenseWebpackPlugin(plugin);
                    this._addImport('license-webpack-plugin', 'LicenseWebpackPlugin');
                    break;
                case ConcatPlugin:
                    args = this._concatPlugin(plugin);
                    this.variableImports['webpack-concat-plugin'] = 'ConcatPlugin';
                    break;
                case UglifyJSPlugin:
                    args = this._uglifyjsPlugin(plugin);
                    this.variableImports['uglifyjs-webpack-plugin'] = 'UglifyJsPlugin';
                    break;
                case SubresourceIntegrityPlugin:
                    this.variableImports['webpack-subresource-integrity'] = 'SubresourceIntegrityPlugin';
                    break;
                default:
                    if (plugin.constructor.name == 'AngularServiceWorkerPlugin') {
                        this._addImport('@angular/service-worker/build/webpack', plugin.constructor.name);
                    }
                    else if (plugin['copyWebpackPluginPatterns']) {
                        // CopyWebpackPlugin doesn't have a constructor nor save args.
                        this.variableImports['copy-webpack-plugin'] = 'CopyWebpackPlugin';
                        const patternOptions = plugin['copyWebpackPluginPatterns'].map((pattern) => {
                            if (!pattern.context) {
                                return pattern;
                            }
                            const context = path.relative(process.cwd(), pattern.context);
                            return Object.assign({}, pattern, { context });
                        });
                        const patternsSerialized = serializer(patternOptions);
                        const optionsSerialized = serializer(plugin['copyWebpackPluginOptions']) || 'undefined';
                        return `\uFF02CopyWebpackPlugin(${patternsSerialized}, ${optionsSerialized})\uFF02`;
                    }
                    break;
            }
            const argsSerialized = serializer(args) || '';
            return `\uFF02${plugin.constructor.name}(${argsSerialized})\uFF02`;
        });
    }
    _resolveReplacer(value) {
        this.variableImports['rxjs/_esm5/path-mapping'] = 'rxPaths';
        return Object.assign({}, value, {
            modules: value.modules.map((x) => './' + path.relative(this._root, x)),
            alias: this._escape('rxPaths()')
        });
    }
    _outputReplacer(value) {
        return Object.assign({}, value, {
            path: this._relativePath('process.cwd()', path.relative(this._root, value.path))
        });
    }
    _path(l) {
        return l.split('!').map(x => {
            return path.isAbsolute(x) ? './' + path.relative(this._root, x) : x;
        }).join('!');
    }
    _entryReplacer(value) {
        const newValue = Object.assign({}, value);
        for (const key of Object.keys(newValue)) {
            newValue[key] = newValue[key].map((l) => this._path(l));
        }
        return newValue;
    }
    _loaderReplacer(loader) {
        if (typeof loader == 'string') {
            if (loader.match(/\/node_modules\/extract-text-webpack-plugin\//)) {
                return 'extract-text-webpack-plugin';
            }
            else if (loader.match(/@ngtools\/webpack\/src\/index.ts/)) {
                // return '@ngtools/webpack';
            }
        }
        else {
            if (loader.loader) {
                loader.loader = this._loaderReplacer(loader.loader);
            }
            if (loader.loader === 'postcss-loader' && !this._postcssProcessed) {
                const args = loader.options.plugins[exports.postcssArgs];
                Object.keys(args.variableImports)
                    .forEach(key => this.variableImports[key] = args.variableImports[key]);
                Object.keys(args.variables)
                    .forEach(key => this.variables[key] = JSON.stringify(args.variables[key]));
                this.variables['postcssPlugins'] = loader.options.plugins;
                loader.options.plugins = this._escape('postcssPlugins');
                this._postcssProcessed = true;
            }
        }
        return loader;
    }
    _ruleReplacer(value) {
        const replaceExcludeInclude = (v) => {
            if (typeof v == 'object') {
                if (v.constructor == RegExp) {
                    return this._serializeRegExp(v);
                }
                return v;
            }
            else if (typeof v == 'string') {
                if (v === path.join(this._root, 'node_modules')) {
                    return this._serializeRegExp(/(\\|\/)node_modules(\\|\/)/);
                }
                return this._relativePath('process.cwd()', path.relative(this._root, v));
            }
            else {
                return v;
            }
        };
        if (value[exports.pluginArgs]) {
            return {
                include: Array.isArray(value.include)
                    ? value.include.map((x) => replaceExcludeInclude(x))
                    : replaceExcludeInclude(value.include),
                test: this._serializeRegExp(value.test),
                loaders: this._escape(`ExtractTextPlugin.extract(${JSON.stringify(value[exports.pluginArgs], null, 2)})`)
            };
        }
        if (value.loaders) {
            value.loaders = value.loaders.map((loader) => this._loaderReplacer(loader));
        }
        if (value.loader) {
            value.loader = this._loaderReplacer(value.loader);
        }
        if (value.use) {
            if (Array.isArray(value.use)) {
                value.use = value.use.map((loader) => this._loaderReplacer(loader));
            }
            else {
                value.use = this._loaderReplacer(value.loader);
            }
        }
        if (value.exclude) {
            value.exclude = Array.isArray(value.exclude)
                ? value.exclude.map((x) => replaceExcludeInclude(x))
                : replaceExcludeInclude(value.exclude);
        }
        if (value.include) {
            value.include = Array.isArray(value.include)
                ? value.include.map((x) => replaceExcludeInclude(x))
                : replaceExcludeInclude(value.include);
        }
        return value;
    }
    _moduleReplacer(value) {
        return Object.assign({}, value, {
            rules: value.rules && value.rules.map((x) => this._ruleReplacer(x))
        });
    }
    _globReplacer(value) {
        return Object.assign({}, value, {
            cwd: this._relativePath('process.cwd()', path.relative(this._root, value.cwd))
        });
    }
    _replacer(_key, value) {
        if (value === undefined) {
            return value;
        }
        if (value === null) {
            return null;
        }
        if (value.constructor === RegExp) {
            return this._serializeRegExp(value);
        }
        return value;
    }
    serialize(config) {
        // config = Object.assign({}, config);
        config['plugins'] = this._pluginsReplacer(config['plugins']);
        // Routes using PathLocationStrategy break without this.
        config['devServer'] = {
            'historyApiFallback': true
        };
        config['resolve'] = this._resolveReplacer(config['resolve']);
        config['resolveLoader'] = this._resolveReplacer(config['resolveLoader']);
        config['entry'] = this._entryReplacer(config['entry']);
        config['output'] = this._outputReplacer(config['output']);
        config['module'] = this._moduleReplacer(config['module']);
        config['context'] = undefined;
        return JSON.stringify(config, (k, v) => this._replacer(k, v), 2)
            .replace(/"\uFF01(.*?)\uFF01"/g, (_, v) => {
            return JSON.parse(`"${v}"`);
        })
            .replace(/(\s*)(.*?)"\uFF02(.*?)\uFF02"(,?).*/g, (_, indent, key, value, comma) => {
            const ctor = JSON.parse(`"${value}"`).split(/\n+/g).join(indent);
            return `${indent}${key}new ${ctor}${comma}`;
        })
            .replace(/"\uFF01(.*?)\uFF01"/g, (_, v) => {
            return JSON.parse(`"${v}"`);
        });
    }
    generateVariables() {
        let variableOutput = '';
        Object.keys(this.variableImports)
            .forEach((key) => {
            const [module, name] = key.split(/\./);
            variableOutput += `const ${this.variableImports[key]} = require` + `('${module}')`;
            if (name) {
                variableOutput += '.' + name;
            }
            variableOutput += ';\n';
        });
        variableOutput += '\n';
        Object.keys(this.imports)
            .forEach((key) => {
            const [module, name] = key.split(/\./);
            variableOutput += `const { ${this.imports[key].join(', ')} } = require` + `('${module}')`;
            if (name) {
                variableOutput += '.' + name;
            }
            variableOutput += ';\n';
        });
        variableOutput += '\n';
        Object.keys(this.variables)
            .forEach((key) => {
            variableOutput += `const ${key} = ${this.variables[key]};\n`;
        });
        variableOutput += '\n\n';
        return variableOutput;
    }
}
exports.default = Task.extend({
    run: function (runTaskOptions) {
        const project = this.project;
        const cliConfig = config_1.CliConfig.fromProject();
        const config = cliConfig.config;
        const appConfig = app_utils_1.getAppFromConfig(runTaskOptions.app);
        const tsConfigPath = path.join(process.cwd(), appConfig.root, appConfig.tsconfig);
        const outputPath = runTaskOptions.outputPath || appConfig.outDir;
        const force = runTaskOptions.force;
        if (project.root === path.resolve(outputPath)) {
            throw new SilentError('Output path MUST not be project root directory!');
        }
        if (appConfig.platform === 'server') {
            throw new SilentError('ng eject for platform server applications is coming soon!');
        }
        const webpackConfig = new webpack_config_1.NgCliWebpackConfig(runTaskOptions, appConfig).buildConfig();
        const serializer = new JsonWebpackSerializer(process.cwd(), outputPath, appConfig.root);
        const output = serializer.serialize(webpackConfig);
        const webpackConfigStr = `${serializer.generateVariables()}\n\nmodule.exports = ${output};\n`;
        return Promise.resolve()
            .then(() => exists('webpack.config.js'))
            .then(webpackConfigExists => {
            if (webpackConfigExists && !force) {
                throw new SilentError('The webpack.config.js file already exists.');
            }
        })
            .then(() => strip_bom_1.stripBom(fs.readFileSync('package.json', 'utf-8')))
            .then((packageJson) => JSON.parse(packageJson))
            .then((packageJson) => {
            const scripts = packageJson['scripts'];
            if (scripts['build'] && scripts['build'] !== 'ng build' && !force) {
                throw new SilentError(common_tags_1.oneLine `
            Your package.json scripts must not contain a build script as it will be overwritten.
          `);
            }
            if (scripts['start'] && scripts['start'] !== 'ng serve' && !force) {
                throw new SilentError(common_tags_1.oneLine `
            Your package.json scripts must not contain a start script as it will be overwritten.
          `);
            }
            if (scripts['pree2e'] && scripts['pree2e'] !== pree2eNpmScript && !force) {
                throw new SilentError(common_tags_1.oneLine `
            Your package.json scripts must not contain a pree2e script as it will be
            overwritten.
          `);
            }
            if (scripts['e2e'] && scripts['e2e'] !== 'ng e2e' && !force) {
                throw new SilentError(common_tags_1.oneLine `
            Your package.json scripts must not contain a e2e script as it will be overwritten.
          `);
            }
            if (scripts['test'] && scripts['test'] !== 'ng test' && !force) {
                throw new SilentError(common_tags_1.oneLine `
            Your package.json scripts must not contain a test script as it will be overwritten.
          `);
            }
            packageJson['scripts']['build'] = 'webpack';
            packageJson['scripts']['start'] = 'webpack-dev-server --port=4200';
            packageJson['scripts']['test'] = 'karma start ./karma.conf.js';
            packageJson['scripts']['pree2e'] = pree2eNpmScript;
            packageJson['scripts']['e2e'] = 'protractor ./protractor.conf.js';
            // Add new dependencies based on our dependencies.
            const ourPackageJson = require('../package.json');
            if (!packageJson['devDependencies']) {
                packageJson['devDependencies'] = {};
            }
            packageJson['devDependencies']['webpack-dev-server']
                = ourPackageJson['dependencies']['webpack-dev-server'];
            // Update all loaders from webpack, plus postcss plugins.
            [
                'webpack',
                'autoprefixer',
                'css-loader',
                'cssnano',
                'exports-loader',
                'file-loader',
                'html-webpack-plugin',
                'json-loader',
                'karma-sourcemap-loader',
                'less-loader',
                'postcss-loader',
                'postcss-url',
                'raw-loader',
                'sass-loader',
                'source-map-loader',
                'istanbul-instrumenter-loader',
                'style-loader',
                'stylus-loader',
                'url-loader',
                'circular-dependency-plugin',
                'webpack-concat-plugin',
                'copy-webpack-plugin',
                'uglifyjs-webpack-plugin',
            ].forEach((packageName) => {
                packageJson['devDependencies'][packageName] = ourPackageJson['dependencies'][packageName];
            });
            return writeFile('package.json', JSON.stringify(packageJson, null, 2) + '\n');
        })
            .then(() => JSON.parse(strip_bom_1.stripBom(fs.readFileSync(tsConfigPath, 'utf-8'))))
            .then((tsConfigJson) => {
            if (!tsConfigJson.exclude || force) {
                // Make sure we now include tests.  Do not touch otherwise.
                tsConfigJson.exclude = [
                    'test.ts',
                    '**/*.spec.ts'
                ];
            }
            return writeFile(tsConfigPath, JSON.stringify(tsConfigJson, null, 2) + '\n');
        })
            .then(() => writeFile('webpack.config.js', webpackConfigStr))
            .then(() => {
            // Update the CLI Config.
            config.project.ejected = true;
            cliConfig.save();
        })
            .then(() => {
            console.log(yellow(common_tags_1.stripIndent `
          ==========================================================================================
          Ejection was successful.

          To run your builds, you now need to do the following commands:
             - "npm run build" to build.
             - "npm test" to run unit tests.
             - "npm start" to serve the app using webpack-dev-server.
             - "npm run e2e" to run protractor.

          Running the equivalent CLI commands will result in an error.

          ==========================================================================================
          Some packages were added. Please run "npm install".
        `));
        });
    }
});
//# sourceMappingURL=/users/hansl/sources/hansl/angular-cli/tasks/eject.js.map