8000 feat(@angular/cli): add an eject command · angular/angular-cli@3be7060 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3be7060

Browse files
committed
feat(@angular/cli): add an eject command
The command will generate a webpack.config.js and will add scripts to the package.json.
1 parent c75eda7 commit 3be7060

File tree

9 files changed

+370
-6
lines changed
  • @ngtools/webpack/src
  • plugins
  • 9 files changed

    +370
    -6
    lines changed

    packages/@angular/cli/addon/index.js

    Lines changed: 1 addition & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -21,6 +21,7 @@ module.exports = {
    2121
    return {
    2222
    'build': require('../commands/build').default,
    2323
    'serve': require('../commands/serve').default,
    24+
    'eject': require('../commands/eject').default,
    2425
    'new': require('../commands/new').default,
    2526
    'generate': require('../commands/generate').default,
    2627
    'destroy': require('../commands/destroy').default,
    Lines changed: 64 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,64 @@
    1+
    import { BuildOptions } from '../models/build-options';
    2+
    import { Version } from '../upgrade/version';
    3+
    4+
    const Command = require('../ember-cli/lib/models/command');
    5+
    6+
    // defaults for BuildOptions
    7+
    export const baseEjectCommandOptions: any = [
    8+
    {
    9+
    name: 'target',
    10+
    type: String,
    11+
    default: 'development',
    12+
    aliases: ['t', { 'dev': 'development' }, { 'prod': 'production' }]
    13+
    },
    14+
    { name: 'environment', type: String, aliases: ['e'] },
    15+
    { name: 'output-path', type: 'Path', aliases: ['op'] },
    16+
    { name: 'a F438 ot', type: Boolean },
    17+
    { name: 'sourcemap', type: Boolean, aliases: ['sm', 'sourcemaps'] },
    18+
    { name: 'vendor-chunk', type: Boolean, default: true, aliases: ['vc'] },
    19+
    { name: 'base-href', type: String, aliases: ['bh'] },
    20+
    { name: 'deploy-url', type: String, aliases: ['d'] },
    21+
    { name: 'verbose', type: Boolean, default: false, aliases: ['v'] },
    22+
    { name: 'progress', type: Boolean, default: true, aliases: ['pr'] },
    23+
    { name: 'i18n-file', type: String },
    24+
    { name: 'i18n-format', type: String },
    25+
    { name: 'locale', type: String },
    26+
    { name: 'extract-css', type: Boolean, aliases: ['ec'] },
    27+
    {
    28+
    name: 'output-hashing',
    29+
    type: String,
    30+
    values: ['none', 'all', 'media', 'bundles'],
    31+
    description: 'define the output filename cache-busting hashing mode',
    32+
    aliases: ['oh']
    33+
    },
    34+
    ];
    35+
    36+
    export interface EjectTaskOptions extends BuildOptions {
    37+
    }
    38+
    39+
    40+
    const EjectCommand = Command.extend({
    41+
    name: 'eject',
    42+
    description: 'Ejects your app and output the proper webpack configuration and scripts.',
    43+
    44+
    availableOptions: baseEjectCommandOptions,
    45+
    46+
    run: function (commandOptions: EjectTaskOptions) {
    47+
    const project = this.project;
    48+
    49+
    // Check angular version.
    50+
    Version.assertAngularVersionIs2_3_1OrHigher(project.root);
    51+
    52+
    const EjectTask = require('../tasks/eject').default;
    53+
    54+
    const ejectTask = new EjectTask({
    55+
    cliProject: project,
    56+
    ui: this.ui,
    57+
    });
    58+
    59+
    return ejectTask.run(commandOptions);
    60+
    }
    61+
    });
    62+
    63+
    64+
    export default EjectCommand;

    packages/@angular/cli/lib/base-href-webpack/base-href-webpack-plugin.ts

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -3,7 +3,7 @@ export interface BaseHrefWebpackPluginOptions {
    33
    }
    44

    55
    export class BaseHrefWebpackPlugin {
    6-
    constructor(private options: BaseHrefWebpackPluginOptions) { }
    6+
    constructor(public readonly options: BaseHrefWebpackPluginOptions) { }
    77

    88
    apply(compiler: any): void {
    99
    // Ignore if baseHref is not passed

    packages/@angular/cli/models/webpack-configs/common.ts

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -9,7 +9,7 @@ import { WebpackConfigOptions } from '../webpack-config';
    99
    const autoprefixer = require('autoprefixer');
    1010
    const ProgressPlugin = require('webpack/lib/ProgressPlugin');
    1111
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    12-
    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    12+
    1313

    1414
    /**
    1515
    * Enumerate loaders and their dependencies from this file to let the dependency validator

    packages/@angular/cli/models/webpack-configs/typescript.ts

    Lines changed: 4 additions & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -48,7 +48,10 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) {
    4848
    export const getNonAotConfig = function(wco: WebpackConfigOptions) {
    4949
    const { projectRoot, appConfig } = wco;
    5050
    let exclude = [ '**/*.spec.ts' ];
    51-
    if (appConfig.test) { exclude.push(path.join(projectRoot, appConfig.root, appConfig.test)); };
    51+
    if (appConfig.test) {
    52+
    exclude.push(path.join(projectRoot, appConfig.root, appConfig.test));
    53+
    }
    54+
    5255
    return {
    5356
    module: {
    5457
    rules: [
    Lines changed: 11 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,11 @@
    1+
    // Exports the webpack plugins we use internally.
    2+
    3+
    module.exports = {
    4+
    BaseHrefWebpackPlugin:
    5+
    require('../lib/base-href-webpack/base-href-webpack-plugin').BaseHrefWebpackPlugin,
    6+
    CompressionPlugin: require('../lib/webpack/compression-plugin').CompressionPlugin,
    7+
    GlobCopyWebpackPlugin: require('../plugins/glob-copy-webpack-plugin').GlobCopyWebpackPlugin,
    8+
    SuppressExtractedTextChunksWebpackPlugin:
    9+
    require('../plugins/suppress-entry-chunks-webpack-plugin')
    10+
    .SuppressExtractedTextChunksWebpackPlugin
    11+
    };

    packages/@angular/cli/tasks/eject.ts

    Lines changed: 276 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,276 @@
    1+
    import * as rimraf from 'rimraf';
    2+
    import * as path from 'path';
    3+
    import * as webpack from 'webpack';
    4+
    5+
    import { BuildTaskOptions } from '../commands/eject';
    6+
    import { NgCliWebpackConfig } from '../models/webpack-config';
    7+
    import { CliConfig } from '../models/config';
    8+
    import { AotPlugin } from '@ngtools/webpack';
    9+
    10+
    const autoprefixer = require('autoprefixer');
    11+
    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    12+
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    13+
    const SilentError = require('silent-error');
    14+
    const Task = require('../ember-cli/lib/models/task');
    15+
    16+
    const CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
    17+
    const LoaderOptionsPlugin = webpack.LoaderOptionsPlugin;
    18+
    19+
    20+
    const header = `const path = require('path');`;
    21+
    const plugins = `
    22+
    const { LoaderOptionsPlugin, NoEmitOnErrorsPlugin, ProgressPlugin } = require('webpack');
    23+
    const { CommonsChunkPlugin } = require('webpack').optimize;
    24+
    const { BaseHrefWebpackPlugin, GlobCopyWebpackPlugin } = require('@angular/cli/plugins/webpack');
    25+
    const { AotPlugin } = require('@ngtools/webpack');
    26+
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    27+
    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    28+
    const autoprefixer = require('autoprefixer');
    29+
    `;
    30+
    const localVariables = `const nodeModules = path.join(process.cwd(), 'node_modules');`;
    31+
    32+
    33+
    34+
    class JsonWebpackSerializer {
    35+
    constructor(private _root: string) {}
    36+
    37+
    private _escape(str: string) {
    38+
    return '\uFF01' + str + '\uFF01';
    39+
    }
    40+
    41+
    private _serializeRegExp(re: RegExp) {
    42+
    return this._escape(re.toString());
    43+
    }
    44+
    45+
    private _serializeFunction(fn: Function) {
    46+
    return this._escape(fn.toString());
    47+
    }
    48+
    49+
    private _commonsChunkPluginSerialize(value: any): any {
    50+
    let minChunks = value.minChunks;
    51+
    switch(typeof minChunks) {
    52+
    case 'function':
    53+
    minChunks = this._serializeFunction(value.minChunks);
    54+
    break;
    55+
    }
    56+
    57+
    return {
    58+
    name: value.chunkNames,
    59+
    filename: value.filenameTemplate,
    60+
    minChunks,
    61+
    chunks: value.selectedChunks,
    62+
    async: value.async,
    63+
    minSize: value.minSize
    64+
    };
    65+
    }
    66+
    67+
    private _extractTextPluginSerialize(value: any): any {
    68+
    return {
    69+
    filename: value.filename,
    70+
    disable: value.options.disable
    71+
    };
    72+
    }
    73+
    74+
    private _aotPluginSerialize(value: AotPlugin): any {
    75+
    return Object.assign({}, value.options, {
    76+
    tsConfigPath: path.relative(this._root, value.options.tsConfigPath),
    77+
    mainPath: path.relative(value.basePath, value.options.mainPath),
    78+
    hostOverrideFileSystem: undefined,
    79+
    exclude: value.options.exclude.map(p => {
    80+
    return p.startsWith('/') ? path.relative(value.basePath, p) : p;
    81+
    })
    82+
    });
    83+
    }
    84+
    85+
    private _htmlWebpackPlugin(value: any) {
    86+
    return Object.assign({}, value.options, {
    87+
    template: './' + path.relative(this._root, value.options.template),
    88+
    filename: './' + path.relative(this._root, value.options.filename)
    89+
    });
    90+
    }
    91+
    92+
    private _loaderOptionsPlugin(plugin: any) {
    93+
    return Object.assign({}, plugin.options, {
    94+
    test: plugin.options.test instanceof RegExp
    95+
    ? this._serializeRegExp(plugin.options.test)
    96+
    : undefined,
    97+
    options: Object.assign({}, plugin.options.options, {
    98+
    context: '',
    99+
    postcss: plugin.options.options.postcss.map(x => {
    100+
    if (x && x.toString() == autoprefixer()) {
    101+
    return this._escape('autoprefixer()');
    102+
    } else {
    103+
    if (typeof x == 'function') {
    104+
    return this._serializeFunction(x);
    105+
    } else {
    106+
    return x;
    107+
    }
    108+
    }
    109+
    })
    110+
    })
    111+
    });
    112+
    }
    113+
    114+
    private _pluginsReplacer(plugins: any[]) {
    115+
    return plugins.map(plugin => {
    116+
    let args = plugin.options || undefined;
    117+
    118+
    switch (plugin.constructor) {
    119+
    case webpack.optimize.CommonsChunkPlugin:
    120+
    args = this._commonsChunkPluginSerialize(plugin);
    121+
    break;
    122+
    case ExtractTextPlugin:
    123+
    args = this._extractTextPluginSerialize(plugin);
    124+
    break;
    125+
    case AotPlugin:
    126+
    args = this._aotPluginSerialize(plugin);
    127+
    break;
    128+
    case HtmlWebpackPlugin:
    129+
    args = this._htmlWebpackPlugin(plugin);
    130+
    break;
    131+
    case LoaderOptionsPlugin:
    132+
    args = this._loaderOptionsPlugin(plugin);
    133+
    break;
    134+
    default:
    135+
    }
    136+
    137+
    const argsSerialized = JSON.stringify(args, (k, v) => this._replacer(k, v), 2) || '';
    138+
    return `\uFF02${plugin.constructor.name}(${argsSerialized})\uFF02`;
    139+
    });
    140+
    }
    141+
    142+
    private _resolveReplacer(value: any) {
    143+
    return Object.assign({}, value, {
    144+
    modules: value.modules.map(x => './' + path.relative(this._root, x))
    145+
    });
    146+
    }
    147+
    148+
    private _outputReplacer(value: any) {
    149+
    return Object.assign({}, value, {
    150+
    path: './' + path.relative(this._root, value.path)
    151+
    });
    152+
    }
    153+
    154+
    private _entryReplacer(value: any) {
    155+
    const newValue = Object.assign({}, value);
    156+
    for (const key of Object.keys(newValue)) {
    157+
    const v = newValue[key];
    158+
    newValue[key] = v.map(x => './' + path.relative(this._root, x));
    159+
    }
    160+
    return newValue;
    161+
    }
    162+
    163+
    private _loaderReplacer(loader: any) {
    164+
    if (typeof loader == 'string') {
    165+
    if (loader.match(/\/node_modules\/extract-text-webpack-plugin\//)) {
    166+
    return 'extract-text-webpack-plugin';
    167+
    } else if (loader.match(/@ngtools\/webpack\/src\/index.ts/)) {
    168+
    // return '@ngtools/webpack';
    169+
    }
    170+
    } else {
    171+
    if (loader.loader) {
    172+
    loader.loader = this._loaderReplacer(loader.loader);
    173+
    }
    174+
    }
    175+
    return loader;
    176+
    }
    177+
    178+
    private _ruleReplacer(value: any) {
    179+
    if (value.loaders) {
    180+
    value.loaders = value.loaders.map(loader => this._loaderReplacer(loader));
    181+
    }
    182+
    if (value.loader) {
    183+
    value.loader = this._loaderReplacer(value.loader);
    184+
    }
    185+
    186+
    const replaceExcludeInclude = (v: any) => {
    187+
    if (typeof v == 'object') {
    188+
    if (v.constructor == RegExp) {
    189+
    return this._serializeRegExp(v);
    190+
    }
    191+
    return v;
    192+
    } else if (typeof v == 'string') {
    193+
    return './' + path.relative(this._root, v);
    194+
    }
    195+
    };
    196+
    197+
    if (value.exclude) {
    198+
    value.exclude = Array.isArray(value.exclude)
    199+
    ? value.exclude.map(x => replaceExcludeInclude(x))
    200+
    : replaceExcludeInclude(value.exclude);
    201+
    }
    202+
    if (value.include) {
    203+
    value.include = Array.isArray(value.include)
    204+
    ? value.include.map(x => replaceExcludeInclude(x))
    205+
    : replaceExcludeInclude(value.include);
    206+
    }
    207+
    208+
    return value;
    209+
    }
    210+
    211+
    private _moduleReplacer(value: any) {
    212+
    return Object.assign({}, value, {
    213+
    rules: value.rules && value.rules.map(x => this._ruleReplacer(x))
    214+
    });
    215+
    }
    216+
    217+
    private _replacer(key: string, value: any) {
    218+
    if (value === undefined) {
    219+
    return value;
    220+
    }
    221+
    if (value === null) {
    222+
    return null;
    223+
    }
    224+
    if (value.constructor === RegExp) {
    225+
    return this._serializeRegExp(value);
    226+
    }
    227+
    228+
    return value;
    229+
    }
    230+
    231+
    serialize(config: any): string {
    232+
    // config = Object.assign({}, config);
    233+
    config['plugins'] = this._pluginsReplacer(config['plugins']);
    234+
    config['resolve'] = this._resolveReplacer(config['resolve']);
    235+
    config['resolveLoader'] = this._resolveReplacer(config['resolveLoader']);
    236+
    config['entry'] = this._entryReplacer(config['entry']);
    237+
    config['output'] = this._outputReplacer(config['output']);
    238+
    config['module'] = this._moduleReplacer(config['module']);
    239+
    config['context'] = undefined;
    240+
    241+
    return JSON.stringify(config, (k, v) => this._replacer(k, v), 2)
    242+
    .replace(/"\uFF01(.*?)\uFF01"/g, (_, v) => {
    243+
    return JSON.parse(`"${v}"`);
    244+
    })
    245+
    .replace(/(\s*)(.*?)"\uFF02(.*?)\uFF02"(,?).*/g, (_, indent, key, value, comma) => {
    246+
    const ctor = JSON.parse(`"${value}"`).split(/\n+/g).join(indent);
    247+
    return `${indent}${key}new ${ctor}${comma}`;
    248+
    })
    249+
    .replace(/"\uFF01(.*?)\uFF01"/g, (_, v) => {
    250+
    return JSON.parse(`"${v}"`);
    251+
    });
    252+
    }
    253+
    }
    254+
    255+
    256+
    export default Task.extend({
    257+
    run: function (runTaskOptions: BuildTaskOptions) {
    258+
    const project = this.cliProject;
    259+
    const outputPath = runTaskOptions.outputPath || CliConfig.fromProject().config.apps[0].outDir;
    260+
    261+
    if (project.root === outputPath) {
    262+
    throw new SilentError ('Output path MUST not be project root directory!');
    263+
    8210 }
    264+
    rimraf.sync(path.resolve(project.root, outputPath));
    265+
    266+
    const webpackConfig = new NgCliWebpackConfig(runTaskOptions).config;
    267+
    const serializer = new JsonWebpackSerializer(process.cwd());
    268+
    console.log(header + '\n\n'
    269+
    + plugins
    270+
    + localVariables
    271+
    + '\n\n'
    272+
    + 'module.exports = ' + serializer.serialize(webpackConfig) + ';\n');
    273+
    274+
    return Promise.resolve();
    275+
    }
    276+
    });

    0 commit comments

    Comments
     (0)
    0