From a7aa4122fe4b9b52bac54818c2bee93a2f5317fb Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 16 Feb 2017 12:14:53 -0800 Subject: [PATCH 1/8] chore: fix typings for Jasmine and TS 2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e23e096cdb3..974b5093eb6d 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "@types/express": "^4.0.32", "@types/fs-extra": "0.0.37", "@types/glob": "^5.0.29", - "@types/jasmine": "^2.2.32", + "@types/jasmine": "~2.2.0", "@types/lodash": "4.14.50", "@types/mock-fs": "^3.6.30", "@types/node": "^6.0.36", From 3427cdc7feae6a39f9c16e36c09bfa44dd6a344f Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 16 Feb 2017 12:23:46 -0800 Subject: [PATCH 2/8] refactor: removed unused import --- packages/@angular/cli/models/webpack-configs/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@angular/cli/models/webpack-configs/common.ts b/packages/@angular/cli/models/webpack-configs/common.ts index 44275c9c4eb1..e3e0d257d121 100644 --- a/packages/@angular/cli/models/webpack-configs/common.ts +++ b/packages/@angular/cli/models/webpack-configs/common.ts @@ -9,7 +9,7 @@ import { WebpackConfigOptions } from '../webpack-config'; const autoprefixer = require('autoprefixer'); const ProgressPlugin = require('webpack/lib/ProgressPlugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const ExtractTextPlugin = require('extract-text-webpack-plugin'); + /** * Enumerate loaders and their dependencies from this file to let the dependency validator From 2420b0b33fcd17b2263371a9a611b1e57040d34a Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 16 Feb 2017 12:25:08 -0800 Subject: [PATCH 3/8] fix(@angular/cli): lint now works with excluded files. --- packages/@angular/cli/tasks/lint.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/@angular/cli/tasks/lint.ts b/packages/@angular/cli/tasks/lint.ts index 403f917c8c4b..6cd8c1ab31d6 100644 --- a/packages/@angular/cli/tasks/lint.ts +++ b/packages/@angular/cli/tasks/lint.ts @@ -48,7 +48,11 @@ export default Task.extend({ }, program); files.forEach((file) => { - const fileContents = program.getSourceFile(file).getFullText(); + const sourceFile = program.getSourceFile(file); + if (!sourceFile) { + return; + } + const fileContents = sourceFile.getFullText(); const configLoad = Configuration.findConfiguration(config.tslintConfig, file); linter.lint(file, fileContents, configLoad.results); }); From 5d3c5dc8a8d8a06e1c9557b4f26b4229e42fb76a Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 16 Feb 2017 12:31:27 -0800 Subject: [PATCH 4/8] feat(@ngtools/webpack): add an option to redirect files It takes multiple paths and not the content, which is cleaner in a configuration. --- .../cli/models/webpack-configs/typescript.ts | 14 +++++++++----- packages/@ngtools/webpack/src/plugin.ts | 19 ++++++++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/@angular/cli/models/webpack-configs/typescript.ts b/packages/@angular/cli/models/webpack-configs/typescript.ts index 6eda860b0f40..b3fac77f34ca 100644 --- a/packages/@angular/cli/models/webpack-configs/typescript.ts +++ b/packages/@angular/cli/models/webpack-configs/typescript.ts @@ -17,7 +17,7 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) { const { appConfig, projectRoot, buildOptions } = wco; // Read the environment, and set it in the compiler host. - let hostOverrideFileSystem: any = {}; + let hostReplacementPaths: any = {}; // process environment file replacement if (appConfig.environments) { if (!appConfig.environmentSource) { @@ -58,9 +58,10 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) { const appRoot = path.resolve(projectRoot, appConfig.root); const sourcePath = appConfig.environmentSource; const envFile = appConfig.environments[buildOptions.environment]; - const environmentContent = fs.readFileSync(path.join(appRoot, envFile)).toString(); - hostOverrideFileSystem = { [path.join(appRoot, sourcePath)]: environmentContent }; + hostReplacementPaths = { + [path.join(appRoot, sourcePath)]: path.join(appRoot, envFile) + }; } return new AotPlugin(Object.assign({}, { @@ -69,7 +70,7 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) { i18nFile: buildOptions.i18nFile, i18nFormat: buildOptions.i18nFormat, locale: buildOptions.locale, - hostOverrideFileSystem + hostReplacementPaths }, options)); } @@ -77,7 +78,10 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) { export const getNonAotConfig = function(wco: WebpackConfigOptions) { const { projectRoot, appConfig } = wco; let exclude = [ '**/*.spec.ts' ]; - if (appConfig.test) { exclude.push(path.join(projectRoot, appConfig.root, appConfig.test)); }; + if (appConfig.test) { + exclude.push(path.join(projectRoot, appConfig.root, appConfig.test)); + } + return { module: { rules: [ diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts index 249f61326376..e26f9ab47a72 100644 --- a/packages/@ngtools/webpack/src/plugin.ts +++ b/packages/@ngtools/webpack/src/plugin.ts @@ -25,6 +25,7 @@ export interface AotPluginOptions { typeChecking?: boolean; skipCodeGeneration?: boolean; hostOverrideFileSystem?: { [path: string]: string }; + hostReplacementPaths?: { [path: string]: string }; i18nFile?: string; i18nFormat?: string; locale?: string; @@ -35,6 +36,8 @@ export interface AotPluginOptions { export class AotPlugin implements Tapable { + private _options: AotPluginOptions; + private _compilerOptions: ts.CompilerOptions; private _angularCompilerOptions: AngularCompilerOptions; private _program: ts.Program; @@ -62,9 +65,11 @@ export class AotPlugin implements Tapable { private _firstRun = true; constructor(options: AotPluginOptions) { - this._setupOptions(options); + this._options = Object.assign({}, options); + this._setupOptions(this._options); } + get options() { return this._options; } get basePath() { return this._basePath; } get compilation() { return this._compilation; } get compilerHost() { return this._compilerHost; } @@ -175,6 +180,14 @@ export class AotPlugin implements Tapable { this._compilerHost.writeFile(filePath, options.hostOverrideFileSystem[filePath], false); } } + // Override some files in the FileSystem with paths from the actual file system. + if (options.hasOwnProperty('hostReplacementPaths')) { + for (const filePath of Object.keys(options.hostReplacementPaths)) { + const replacementFilePath = options.hostReplacementPaths[filePath]; + const content = this._compilerHost.readFile(replacementFilePath); + this._compilerHost.writeFile(filePath, content, false); + } + } this._program = ts.createProgram( this._rootFilePath, this._compilerOptions, this._compilerHost); @@ -193,8 +206,8 @@ export class AotPlugin implements Tapable { // still no _entryModule? => try to resolve from mainPath if (!this._entryModule && options.mainPath) { - this._entryModule = resolveEntryModuleFromMain(options.mainPath, this._compilerHost, - this._program); + const mainPath = path.resolve(basePath, options.mainPath); + this._entryModule = resolveEntryModuleFromMain(mainPath, this._compilerHost, this._program); } if (options.hasOwnProperty('i18nFile')) { From d3f9dfd62ab98a50204acee38c76a08b50d7f3b5 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 16 Feb 2017 12:37:08 -0800 Subject: [PATCH 5/8] test: e2e exec() now returns an object {stdout,stderr} Before it was only a string with stdout. --- tests/e2e/tests/commands/config/get.ts | 12 +++++------ tests/e2e/tests/commands/new/check-yarn.ts | 4 ++-- .../e2e/tests/lint/lint-no-config-section.ts | 8 +++---- tests/e2e/tests/lint/lint-with-exclude.ts | 6 +++--- tests/e2e/tests/lint/lint-with-force.ts | 8 +++---- tests/e2e/tests/lint/lint-with-format.ts | 6 +++--- tests/e2e/tests/lint/lint.ts | 6 +++--- tests/e2e/utils/git.ts | 10 ++++----- tests/e2e/utils/process.ts | 21 ++++++++++++++++++- 9 files changed, 50 insertions(+), 31 deletions(-) diff --git a/tests/e2e/tests/commands/config/get.ts b/tests/e2e/tests/commands/config/get.ts index 5f0aafc10dd7..e866c561040f 100644 --- a/tests/e2e/tests/commands/config/get.ts +++ b/tests/e2e/tests/commands/config/get.ts @@ -7,9 +7,9 @@ export default function() { .then(() => process.chdir('/')) .then(() => expectToFail(() => ng('get', 'defaults.component.inlineStyle'))) .then(() => ng('get', '--global', 'defaults.component.inlineStyle')) - .then(output => { - if (!output.match(/false\n?/)) { - throw new Error(`Expected "false", received "${JSON.stringify(output)}".`); + .then(({ stdout }) => { + if (!stdout.match(/false\n?/)) { + throw new Error(`Expected "false", received "${JSON.stringify(stdout)}".`); } }) .then(() => expectToFail(() => { @@ -17,9 +17,9 @@ export default function() { })) .then(() => ng('set', '--global', 'defaults.component.inlineStyle', 'true')) .then(() => ng('get', '--global', 'defaults.component.inlineStyle')) - .then(output => { - if (!output.match(/true\n?/)) { - throw new Error(`Expected "true", received "${JSON.stringify(output)}".`); + .then(({ stdout }) => { + if (!stdout.match(/true\n?/)) { + throw new Error(`Expected "true", received "${JSON.stringify(stdout)}".`); } }) .then(() => ng('set', '--global', 'defaults.component.inlineStyle', 'false')); diff --git a/tests/e2e/tests/commands/new/check-yarn.ts b/tests/e2e/tests/commands/new/check-yarn.ts index dd8ab9454706..22826ee17926 100644 --- a/tests/e2e/tests/commands/new/check-yarn.ts +++ b/tests/e2e/tests/commands/new/check-yarn.ts @@ -8,9 +8,9 @@ export default function() { .then(() => process.chdir(getGlobalVariable('tmp-root'))) .then(() => ng('set', '--global', 'packageManager=default')) .then(() => ng('new', 'foo')) - .then((stdout) => { + .then(({ stdout }) => { // Assuming yarn is installed and checking for message with yarn. - if (!stdout.toString().match(yarnRegEx)) { + if (!stdout.match(yarnRegEx)) { throw new Error('Should display message to use yarn packageManager'); } }); diff --git a/tests/e2e/tests/lint/lint-no-config-section.ts b/tests/e2e/tests/lint/lint-no-config-section.ts index f30f80aba08c..07d67aef518c 100644 --- a/tests/e2e/tests/lint/lint-no-config-section.ts +++ b/tests/e2e/tests/lint/lint-no-config-section.ts @@ -5,15 +5,15 @@ export default function () { return Promise.resolve() .then(() => ng('set', 'lint', '[]')) .then(() => ng('lint')) - .then((output) => { - if (!output.match(/No lint config\(s\) found\./)) { + .then(({ stdout }) => { + if (!stdout.match(/No lint config\(s\) found\./)) { throw new Error(oneLine` Expected to match "No lint configs found." - in ${output}. + in ${stdout}. `); } - return output; + return stdout; }) .then((output) => { if (!output.match(/If this is not intended, run "ng update"\./)) { diff --git a/tests/e2e/tests/lint/lint-with-exclude.ts b/tests/e2e/tests/lint/lint-with-exclude.ts index 9420d9bc84b9..bcf193acc692 100644 --- a/tests/e2e/tests/lint/lint-with-exclude.ts +++ b/tests/e2e/tests/lint/lint-with-exclude.ts @@ -9,11 +9,11 @@ export default function () { .then(() => ng('set', 'lint.0.exclude', '"**/foo.ts"')) .then(() => writeFile(fileName, 'const foo = "";\n')) .then(() => ng('lint')) - .then((output) => { - if (!output.match(/All files pass linting\./)) { + .then(({ stdout }) => { + if (!stdout.match(/All files pass linting\./)) { throw new Error(oneLine` Expected to match "All files pass linting." - in ${output}. + in ${stdout}. `); } }); diff --git a/tests/e2e/tests/lint/lint-with-force.ts b/tests/e2e/tests/lint/lint-with-force.ts index f5b82f041c9d..1f40b16a2346 100644 --- a/tests/e2e/tests/lint/lint-with-force.ts +++ b/tests/e2e/tests/lint/lint-with-force.ts @@ -8,12 +8,12 @@ export default function () { return Promise.resolve() .then(() => writeFile(fileName, 'const foo = "";\n')) .then(() => ng('lint', '--force')) - .then((output) => { - if (!output.match(/" should be '/)) { - throw new Error(`Expected to match "" should be '" in ${output}.`); + .then(({ stdout }) => { + if (!stdout.match(/" should be '/)) { + throw new Error(`Expected to match "" should be '" in ${stdout}.`); } - return output; + return stdout; }) .then((output) => { if (!output.match(/Lint errors found in the listed files\./)) { diff --git a/tests/e2e/tests/lint/lint-with-format.ts b/tests/e2e/tests/lint/lint-with-format.ts index 01e602e61b08..50ee8b5454a9 100644 --- a/tests/e2e/tests/lint/lint-with-format.ts +++ b/tests/e2e/tests/lint/lint-with-format.ts @@ -8,11 +8,11 @@ export default function () { return Promise.resolve() .then(() => writeFile(fileName, 'const foo = "";\n')) .then(() => ng('lint', '--format=stylish', '--force')) - .then((output) => { - if (!output.match(/1:13 quotemark " should be '/)) { + .then(({ stdout }) => { + if (!stdout.match(/1:13 quotemark " should be '/)) { throw new Error(oneLine` Expected to match "1:13 quotemark " should be '" - in ${output}. + in ${stdout}. `); } }); diff --git a/tests/e2e/tests/lint/lint.ts b/tests/e2e/tests/lint/lint.ts index 8de6eb9f07c0..7d4b3d655164 100644 --- a/tests/e2e/tests/lint/lint.ts +++ b/tests/e2e/tests/lint/lint.ts @@ -3,11 +3,11 @@ import { oneLine } from 'common-tags'; export default function () { return ng('lint') - .then((output) => { - if (!output.match(/All files pass linting\./)) { + .then(({ stdout }) => { + if (!stdout.match(/All files pass linting\./)) { throw new Error(oneLine` Expected to match "All files pass linting." - in ${output}. + in ${stdout}. `); } }); diff --git a/tests/e2e/utils/git.ts b/tests/e2e/utils/git.ts index 875ba385e549..35d7dd876eac 100644 --- a/tests/e2e/utils/git.ts +++ b/tests/e2e/utils/git.ts @@ -8,7 +8,7 @@ export function gitClean() { .then(() => { // Checkout missing files return silentGit('status', '--porcelain') - .then(output => output + .then(({ stdout }) => stdout .split(/[\n\r]+/g) .filter(line => line.match(/^ D/)) .map(line => line.replace(/^\s*\S+\s+/, ''))) @@ -19,8 +19,8 @@ export function gitClean() { export function expectGitToBeClean() { return git('status', '--porcelain') - .then(output => { - if (output != '') { + .then(({ stdout }) => { + if (stdout != '') { throw new Error('Git repo is not clean...'); } }); @@ -29,8 +29,8 @@ export function expectGitToBeClean() { export function gitCommit(message: string) { return git('add', '-A') .then(() => git('status', '--porcelain')) - .then(output => { - if (output != '') { + .then(({ stdout }) => { + if (stdout != '') { return git('commit', '-am', message); } }); diff --git a/tests/e2e/utils/process.ts b/tests/e2e/utils/process.ts index bec5a7d97011..32ff695efaac 100644 --- a/tests/e2e/utils/process.ts +++ b/tests/e2e/utils/process.ts @@ -1,5 +1,7 @@ import * as child_process from 'child_process'; import {blue, yellow} from 'chalk'; +import {getGlobalVariable} from './env'; +import {rimraf, writeFile} from './fs'; const treeKill = require('tree-kill'); @@ -75,7 +77,7 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise p !== childProcess); if (!error) { - resolve(stdout); + resolve({ stdout }); } else { err.message += `${error}...\n\nSTDOUT:\n${stdout}\n`; reject(err); @@ -134,9 +136,26 @@ export function silentExecAndWaitForOutputToMatch(cmd: string, args: string[], m return _exec({ silent: true, waitForMatch: match }, cmd, args); } + +let npmInstalledEject = false; export function ng(...args: string[]) { // Auto-add --no-progress to commands that build the app, otherwise we get thousands of lines. if (['build', 'serve', 'test', 'e2e', 'xi18n'].indexOf(args[0]) != -1) { + // If we have the --eject, use webpack for the test. + const argv = getGlobalVariable('argv'); + if (args[0] == 'build' && argv.eject) { + return silentNg('eject', ...args.slice(1), '--force') + .then(() => { + if (!npmInstalledEject) { + npmInstalledEject = true; + // We need to run npm install on the first eject. + return silentNpm('install'); + } + }) + .then(() => rimraf('dist')) + .then(() => _exec({silent: true}, 'node_modules/.bin/webpack', [])); + } + return silentNg(...args, '--no-progress'); } else { return _exec({}, 'ng', args); From bb891df3b3ba979cef3c94966d55c1a754f769e4 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 16 Feb 2017 12:37:32 -0800 Subject: [PATCH 6/8] feat(@angular/cli): eject command The command will generate a webpack.config.js and will add scripts to the package.json. --- .travis.yml | 3 + packages/@angular/cli/addon/index.js | 1 + packages/@angular/cli/commands/eject.ts | 37 ++ .../base-href-webpack-plugin.ts | 2 +- packages/@angular/cli/lib/config/schema.json | 5 + .../cli/models/webpack-configs/styles.ts | 15 +- packages/@angular/cli/plugins/webpack.js | 10 + packages/@angular/cli/tasks/eject.ts | 504 ++++++++++++++++++ .../cli/utilities/package-chunk-sort.ts | 9 +- plugins/webpack.js | 5 + tests/e2e/tests/build/assets.ts | 10 +- tests/e2e/tests/build/dev-build.ts | 6 +- tests/e2e/tests/build/environment.ts | 7 +- tests/e2e/tests/build/json.ts | 6 + tests/e2e/tests/build/output-dir.ts | 10 +- tests/e2e/tests/build/prod-build.ts | 6 +- tests/e2e/tests/build/rebuild-css-change.ts | 6 + .../tests/build/rebuild-deps-type-check.ts | 5 + tests/e2e/tests/build/rebuild.ts | 5 + tests/e2e/tests/build/service-worker.ts | 9 +- tests/e2e/utils/fs.ts | 14 + tests/e2e_runner.ts | 2 +- 22 files changed, 659 insertions(+), 18 deletions(-) create mode 100644 packages/@angular/cli/commands/eject.ts create mode 100644 packages/@angular/cli/plugins/webpack.js create mode 100644 packages/@angular/cli/tasks/eject.ts create mode 100644 plugins/webpack.js diff --git a/.travis.yml b/.travis.yml index f530c30b1e23..6c3077b9a8e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,9 @@ matrix: - node_js: "6" os: linux env: NODE_SCRIPT="tests/run_e2e.js --glob=tests/build/**" + - node_js: "6" + os: linux + env: NODE_SCRIPT="tests/run_e2e.js --eject --glob=tests/build/**" - node_js: "6" os: linux env: NODE_SCRIPT="tests/run_e2e.js --ignore=**/tests/build/**" diff --git a/packages/@angular/cli/addon/index.js b/packages/@angular/cli/addon/index.js index 35546a970444..31435fe65859 100644 --- a/packages/@angular/cli/addon/index.js +++ b/packages/@angular/cli/addon/index.js @@ -21,6 +21,7 @@ module.exports = { return { 'build': require('../commands/build').default, 'serve': require('../commands/serve').default, + 'eject': require('../commands/eject').default, 'new': require('../commands/new').default, 'generate': require('../commands/generate').default, 'destroy': require('../commands/destroy').default, diff --git a/packages/@angular/cli/commands/eject.ts b/packages/@angular/cli/commands/eject.ts new file mode 100644 index 000000000000..e9e7867b4a88 --- /dev/null +++ b/packages/@angular/cli/commands/eject.ts @@ -0,0 +1,37 @@ +import { BuildOptions } from '../models/build-options'; +import { Version } from '../upgrade/version'; +import {baseBuildCommandOptions} from './build'; + +const Command = require('../ember-cli/lib/models/command'); + +// defaults for BuildOptions +export const baseEjectCommandOptions: any = [ + ...baseBuildCommandOptions, + { name: 'force', 'type': Boolean } +]; + +export interface EjectTaskOptions extends BuildOptions { + force?: boolean; +} + + +const EjectCommand = Command.extend({ + name: 'eject', + description: 'Ejects your app and output the proper webpack configuration and scripts.', + + availableOptions: baseEjectCommandOptions, + + run: function (commandOptions: EjectTaskOptions) { + const project = this.project; + const EjectTask = require('../tasks/eject').default; + const ejectTask = new EjectTask({ + cliProject: project, + ui: this.ui, + }); + + return ejectTask.run(commandOptions); + } +}); + + +export default EjectCommand; diff --git a/packages/@angular/cli/lib/base-href-webpack/base-href-webpack-plugin.ts b/packages/@angular/cli/lib/base-href-webpack/base-href-webpack-plugin.ts index c4b857b87022..76401d9eae45 100644 --- a/packages/@angular/cli/lib/base-href-webpack/base-href-webpack-plugin.ts +++ b/packages/@angular/cli/lib/base-href-webpack/base-href-webpack-plugin.ts @@ -3,7 +3,7 @@ export interface BaseHrefWebpackPluginOptions { } export class BaseHrefWebpackPlugin { - constructor(private options: BaseHrefWebpackPluginOptions) { } + constructor(public readonly options: BaseHrefWebpackPluginOptions) { } apply(compiler: any): void { // Ignore if baseHref is not passed diff --git a/packages/@angular/cli/lib/config/schema.json b/packages/@angular/cli/lib/config/schema.json index f4fd00104ea6..1ae503d05e5e 100644 --- a/packages/@angular/cli/lib/config/schema.json +++ b/packages/@angular/cli/lib/config/schema.json @@ -13,6 +13,11 @@ }, "name": { "type": "string" + }, + "ejected": { + "description": "Whether or not this project was ejected.", + "type": "boolean", + "default": false } }, "additionalProperties": false diff --git a/packages/@angular/cli/models/webpack-configs/styles.ts b/packages/@angular/cli/models/webpack-configs/styles.ts index 99976351b7ba..1c40265bff84 100644 --- a/packages/@angular/cli/models/webpack-configs/styles.ts +++ b/packages/@angular/cli/models/webpack-configs/styles.ts @@ -5,6 +5,7 @@ import { } from '../../plugins/suppress-entry-chunks-webpack-plugin'; import { extraEntryParser, getOutputHashFormat } from './utils'; import { WebpackConfigOptions } from '../webpack-config'; +import { pluginArgs } from '../../tasks/eject'; const cssnano = require('cssnano'); const autoprefixer = require('autoprefixer'); @@ -104,8 +105,8 @@ export function getStylesConfig(wco: WebpackConfigOptions) { // load global css as css files if (globalStylePaths.length > 0) { - rules.push(...baseRules.map(({test, loaders}) => ({ - include: globalStylePaths, test, loaders: ExtractTextPlugin.extract({ + rules.push(...baseRules.map(({test, loaders}) => { + const extractTextPlugin = { use: [ ...commonLoaders, ...loaders @@ -113,8 +114,14 @@ export function getStylesConfig(wco: WebpackConfigOptions) { fallback: 'style-loader', // publicPath needed as a workaround https://github.com/angular/angular-cli/issues/4035 publicPath: '' - }) - }))); + }; + const ret: any = { + include: globalStylePaths, test, loaders: ExtractTextPlugin.extract(extractTextPlugin) + }; + // Save the original options as arguments for eject. + ret[pluginArgs] = extractTextPlugin; + return ret; + })); } // supress empty .js files in css only entry points diff --git a/packages/@angular/cli/plugins/webpack.js b/packages/@angular/cli/plugins/webpack.js new file mode 100644 index 000000000000..0fd22391ab39 --- /dev/null +++ b/packages/@angular/cli/plugins/webpack.js @@ -0,0 +1,10 @@ +// Exports the webpack plugins we use internally. + +module.exports = { + BaseHrefWebpackPlugin: + require('../lib/base-href-webpack/base-href-webpack-plugin').BaseHrefWebpackPlugin, + GlobCopyWebpackPlugin: require('../plugins/glob-copy-webpack-plugin').GlobCopyWebpackPlugin, + SuppressExtractedTextChunksWebpackPlugin: + require('../plugins/suppress-entry-chunks-webpack-plugin') + .SuppressExtractedTextChunksWebpackPlugin +}; diff --git a/packages/@angular/cli/tasks/eject.ts b/packages/@angular/cli/tasks/eject.ts new file mode 100644 index 000000000000..caa891004532 --- /dev/null +++ b/packages/@angular/cli/tasks/eject.ts @@ -0,0 +1,504 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; +import * as webpack from 'webpack'; + +import { EjectTaskOptions } from '../commands/eject'; +import { NgCliWebpackConfig } from '../models/webpack-config'; +import { CliConfig } from '../models/config'; +import { AotPlugin } from '@ngtools/webpack'; +import { yellow } from 'chalk'; + +import denodeify = require('denodeify'); +import {oneLine, stripIndent} from 'common-tags'; + +const exists = (p: string) => Promise.resolve(fs.existsSync(p)); +const writeFile = (denodeify(fs.writeFile) as (...args: any[]) => Promise); +const angularCliPlugins = require('../plugins/webpack'); + + +const autoprefixer = require('autoprefixer'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const SilentError = require('silent-error'); +const Task = require('../ember-cli/lib/models/task'); + +const CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; +const LoaderOptionsPlugin = webpack.LoaderOptionsPlugin; +const ProgressPlugin = require('webpack/lib/ProgressPlugin'); + + +export const pluginArgs = Symbol('plugin-args'); + + +class JsonWebpackSerializer { + public imports: {[name: string]: string[]} = {}; + public variableImports: {[name: string]: string} = { + 'path': 'path' + }; + public variables: {[name: string]: string} = { + 'nodeModules': `path.join(process.cwd(), 'node_modules')`, + }; + + + constructor(private _root: string, private _dist: string) {} + + private _escape(str: string) { + return '\uFF01' + str + '\uFF01'; + } + + private _serializeRegExp(re: RegExp) { + return this._escape(re.toString()); + } + + private _serializeFunction(fn: Function) { + return this._escape(fn.toString()); + } + + private _relativePath(of: string, to: string) { + return this._escape(`path.join(${of}, ${JSON.stringify(to)})`); + } + + private _addImport(module: string, importName: string) { + if (!this.imports[module]) { + this.imports[module] = []; + } + if (this.imports[module].indexOf(importName) == -1) { + this.imports[module].push(importName); + } + } + + private _commonsChunkPluginSerialize(value: any): any { + 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 + }; + } + + private _extractTextPluginSerialize(value: any): any { + return { + filename: value.filename, + disable: value.options.disable + }; + } + + private _aotPluginSerialize(value: AotPlugin): any { + const tsConfigPath = path.relative(this._root, value.options.tsConfigPath); + const basePath = path.dirname(tsConfigPath); + return Object.assign({}, value.options, { + tsConfigPath, + mainPath: path.relative(value.basePath, value.options.mainPath), + hostReplacementPaths: Object.keys(value.options.hostReplacementPaths) + .reduce((acc: any, key: string) => { + 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: any) => { + return p.startsWith('/') ? path.relative(value.basePath, p) : p; + }) + : value.options.exclude + }); + } + + private _htmlWebpackPlugin(value: any) { + 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) + }); + } + + private _loaderOptionsPlugin(plugin: any) { + return Object.assign({}, plugin.options, { + test: plugin.options.test instanceof RegExp + ? this._serializeRegExp(plugin.options.test) + : undefined, + options: Object.assign({}, plugin.options.options, { + context: '', + postcss: plugin.options.options.postcss.map((x: any) => { + if (x && x.toString() == autoprefixer()) { + this.variableImports['autoprefixer'] = 'autoprefixer'; + return this._escape('autoprefixer()'); + } else if (x && x.postcssPlugin == 'cssnano') { + this.variableImports['cssnano'] = 'cssnano'; + return this._escape('cssnano({ safe: true, autoprefixer: false })'); + } else { + if (typeof x == 'function') { + return this._serializeFunction(x); + } else { + return x; + } + } + }) + }) + }); + } + + _definePlugin(plugin: any) { + return plugin.definitions; + } + + private _pluginsReplacer(plugins: any[]) { + return plugins.map(plugin => { + let args = plugin.options || undefined; + + switch (plugin.constructor) { + case ProgressPlugin: + this.variableImports['webpack/lib/ProgressPlugin'] = 'ProgressPlugin'; + break; + case webpack.NoEmitOnErrorsPlugin: + this._addImport('webpack', 'NoEmitOnErrorsPlugin'); + break; + case webpack.optimize.UglifyJsPlugin: + this._addImport('webpack.optimize', 'UglifyJsPlugin'); + break; + case angularCliPlugins.BaseHrefWebpackPlugin: + case angularCliPlugins.GlobCopyWebpackPlugin: + case angularCliPlugins.SuppressExtractedTextChunksWebpackPlugin: + this._addImport('@angular/cli/plugins/webpack', plugin.constructor.name); + 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 AotPlugin: + args = this._aotPluginSerialize(plugin); + this._addImport('@ngtools/webpack', 'AotPlugin'); + break; + case HtmlWebpackPlugin: + args = this._htmlWebpackPlugin(plugin); + this.variableImports['html-webpack-plugin'] = 'HtmlWebpackPlugin'; + break; + case LoaderOptionsPlugin: + args = this._loaderOptionsPlugin(plugin); + this._addImport('webpack', 'LoaderOptionsPlugin'); + break; + case webpack.DefinePlugin: + args = this._definePlugin(plugin); + this._addImport('webpack', 'DefinePlugin'); + break; + + default: + if (plugin.constructor.name == 'AngularServiceWorkerPlugin') { + this._addImport('@angular/service-worker/build/webpack', plugin.constructor.name); + } + break; + + } + + const argsSerialized = JSON.stringify(args, (k, v) => this._replacer(k, v), 2) || ''; + return `\uFF02${plugin.constructor.name}(${argsSerialized})\uFF02`; + }); + } + + private _resolveReplacer(value: any) { + return Object.assign({}, value, { + modules: value.modules.map((x: string) => './' + path.relative(this._root, x)) + }); + } + + private _outputReplacer(value: any) { + return Object.assign({}, value, { + path: this._relativePath('process.cwd()', path.relative(this._root, value.path)) + }); + } + + private _path(l: string) { + return l.split('!').map(x => { + return path.isAbsolute(x) ? './' + path.relative(this._root, x) : x; + }).join('!'); + } + + private _entryReplacer(value: any) { + const newValue = Object.assign({}, value); + for (const key of Object.keys(newValue)) { + newValue[key] = newValue[key].map((l: string) => this._path(l)); + } + return newValue; + } + + private _loaderReplacer(loader: any) { + 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); + } + } + return loader; + } + + private _ruleReplacer(value: any) { + const replaceExcludeInclude = (v: any) => { + 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[pluginArgs]) { + return { + include: Array.isArray(value.include) + ? value.include.map((x: any) => replaceExcludeInclude(x)) + : replaceExcludeInclude(value.include), + test: this._serializeRegExp(value.test), + loaders: this._escape( + `ExtractTextPlugin.extract(${JSON.stringify(value[pluginArgs], null, 2)})`) + }; + } + + if (value.loaders) { + value.loaders = value.loaders.map((loader: any) => this._loaderReplacer(loader)); + } + if (value.loader) { + value.loader = this._loaderReplacer(value.loader); + } + + if (value.exclude) { + value.exclude = Array.isArray(value.exclude) + ? value.exclude.map((x: any) => replaceExcludeInclude(x)) + : replaceExcludeInclude(value.exclude); + } + if (value.include) { + value.include = Array.isArray(value.include) + ? value.include.map((x: any) => replaceExcludeInclude(x)) + : replaceExcludeInclude(value.include); + } + + return value; + } + + private _moduleReplacer(value: any) { + return Object.assign({}, value, { + rules: value.rules && value.rules.map((x: any) => this._ruleReplacer(x)) + }); + } + + private _replacer(key: string, value: any) { + if (value === undefined) { + return value; + } + if (value === null) { + return null; + } + if (value.constructor === RegExp) { + return this._serializeRegExp(value); + } + + return value; + } + + serialize(config: any): string { + // config = Object.assign({}, config); + config['plugins'] = this._pluginsReplacer(config['plugins']); + 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(): string { + let variableOutput = ''; + Object.keys(this.variableImports) + .forEach((key: string) => { + 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: string) => { + 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: string) => { + variableOutput += `const ${key} = ${this.variables[key]};\n`; + }); + variableOutput += '\n\n'; + + return variableOutput; + } +} + + +export default Task.extend({ + run: function (runTaskOptions: EjectTaskOptions) { + const project = this.cliProject; + const cliConfig = CliConfig.fromProject(); + const config = cliConfig.config; + const tsConfigPath = path.join(process.cwd(), config.apps[0].root, config.apps[0].tsconfig); + const outputPath = runTaskOptions.outputPath || config.apps[0].outDir; + const force = runTaskOptions.force; + + if (project.root === outputPath) { + throw new SilentError ('Output path MUST not be project root directory!'); + } + + const webpackConfig = new NgCliWebpackConfig(runTaskOptions).config; + const serializer = new JsonWebpackSerializer(process.cwd(), outputPath); + 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.'); + } + }) + // Read the package.json and update it to include npm scripts. We do this first so that if + // an error already exists + .then(() => ts.sys.readFile('package.json')) + .then((packageJson: string) => JSON.parse(packageJson)) + .then((packageJson: any) => { + const scripts = packageJson['scripts']; + if (scripts['build'] && scripts['build'] !== 'ng build' && !force) { + throw new SilentError(oneLine` + Your package.json scripts needs to not contain a build script as it will be overwritten. + `); + } + if (scripts['start'] && scripts['start'] !== 'ng serve' && !force) { + throw new SilentError(oneLine` + Your package.json scripts needs to not contain a start script as it will be overwritten. + `); + } + if (scripts['e2e'] && scripts['e2e'] !== 'ng e2e' && !force) { + throw new SilentError(oneLine` + Your package.json scripts needs to not contain a e2e script as it will be overwritten. + `); + } + if (scripts['test'] && scripts['test'] !== 'ng test' && !force) { + throw new SilentError(oneLine` + Your package.json scripts needs to not contain a test script as it will be overwritten. + `); + } + + packageJson['scripts']['build'] = 'webpack'; + packageJson['scripts']['start'] = 'webpack-dev-server'; + packageJson['scripts']['test'] = 'karma start ./karma.conf.js'; + packageJson['scripts']['e2e'] = 'protractor ./protractor.conf.js'; + + // Add new dependencies based on our dependencies. + const ourPackageJson = require('../package.json'); + packageJson['devDependencies']['webpack-dev-server'] + = ourPackageJson['dependencies']['webpack-dev-server']; + + // Update all loaders from webpack. + [ + 'css-loader', + 'exports-loader', + 'file-loader', + 'json-loader', + 'karma-sourcemap-loader', + 'less-loader', + 'postcss-loader', + 'raw-loader', + 'sass-loader', + 'script-loader', + 'source-map-loader', + 'istanbul-instrumenter-loader', + 'style-loader', + 'stylus-loader', + 'url-loader', + ].forEach((packageName: string) => { + packageJson['devDependencies'][packageName] = ourPackageJson['dependencies'][packageName]; + }); + + return writeFile('package.json', JSON.stringify(packageJson, null, 2)); + }) + .then(() => JSON.parse(ts.sys.readFile(tsConfigPath))) + .then((tsConfigJson: any) => { + 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)); + }) + // Output the webpack.config.js. + .then(() => writeFile('webpack.config.js', webpackConfigStr)) + .then(() => { + // Update the CLI Config. + config.project.ejected = true; + cliConfig.save(); + }) + .then(() => { + console.log(yellow(stripIndent` + ========================================================================================== + Ejection was successful. + + To run your builds, you now need to do the following commands: + - "npm run build" to build. + - "npm run 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". + `)); + }); + } +}); diff --git a/packages/@angular/cli/utilities/package-chunk-sort.ts b/packages/@angular/cli/utilities/package-chunk-sort.ts index 170d9e2b72a0..2d64707c28bf 100644 --- a/packages/@angular/cli/utilities/package-chunk-sort.ts +++ b/packages/@angular/cli/utilities/package-chunk-sort.ts @@ -21,7 +21,7 @@ export function packageChunkSort(appConfig: any) { entryPoints.push(...['vendor', 'main']); - return function sort(left: any, right: any) { + function sort(left: any, right: any) { let leftIndex = entryPoints.indexOf(left.names[0]); let rightindex = entryPoints.indexOf(right.names[0]); @@ -32,5 +32,10 @@ export function packageChunkSort(appConfig: any) { } else { return 0; } - }; + } + + // We need to list of entry points for the Ejected webpack config to work (we reuse the function + // defined above). + (sort as any).entryPoints = entryPoints; + return sort; } diff --git a/plugins/webpack.js b/plugins/webpack.js new file mode 100644 index 000000000000..959c93437d69 --- /dev/null +++ b/plugins/webpack.js @@ -0,0 +1,5 @@ +// This file is necessary when using a linked angular-cli to this repo, meaning that +// require('@angular/cli/plugins/webpack') will load this file, and we just forward to +// the actual published file. +require('../lib/bootstrap-local'); +module.exports = require('../packages/@angular/cli/plugins/webpack'); diff --git a/tests/e2e/tests/build/assets.ts b/tests/e2e/tests/build/assets.ts index a513878fb626..410b923ee16a 100644 --- a/tests/e2e/tests/build/assets.ts +++ b/tests/e2e/tests/build/assets.ts @@ -7,9 +7,13 @@ import { import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; import { expectToFail } from '../../utils/utils'; +import {getGlobalVariable} from '../../utils/env'; export default function () { + // Disable parts of it in webpack tests. + const ejected = getGlobalVariable('argv').eject; + return Promise.resolve() .then(_ => createDir('./src/folder')) .then(_ => createDir('./node_modules/some-package/')) @@ -44,7 +48,7 @@ export default function () { // .gitkeep shouldn't be copied. .then(() => expectToFail(() => expectFileToExist('dist/assets/.gitkeep'))) // Update app to test assets are present. - .then(_ => writeMultipleFiles({ + .then(_ => !ejected && writeMultipleFiles({ 'src/app/app.component.ts': ` import { Component } from '@angular/core'; import { Http, Response } from '@angular/http'; @@ -108,6 +112,6 @@ export default function () { }); });`, })) - .then(() => ng('test', '--single-run')) - .then(() => ng('e2e', '--no-progress')); + .then(() => !ejected && ng('test', '--single-run')) + .then(() => !ejected && ng('e2e', '--no-progress')); } diff --git a/tests/e2e/tests/build/dev-build.ts b/tests/e2e/tests/build/dev-build.ts index f6f96df9deb1..85eb154ab36e 100644 --- a/tests/e2e/tests/build/dev-build.ts +++ b/tests/e2e/tests/build/dev-build.ts @@ -1,10 +1,14 @@ import {ng} from '../../utils/process'; import {expectFileToMatch} from '../../utils/fs'; import {expectGitToBeClean} from '../../utils/git'; +import {getGlobalVariable} from '../../utils/env'; export default function() { + const ejected = getGlobalVariable('argv').eject; + return ng('build', '--env=dev') .then(() => expectFileToMatch('dist/index.html', 'main.bundle.js')) - .then(() => expectGitToBeClean()); + // If this is an ejected test, the eject will create files so git will not be clean. + .then(() => !ejected && expectGitToBeClean()); } diff --git a/tests/e2e/tests/build/environment.ts b/tests/e2e/tests/build/environment.ts index 734461a2e0a5..e6e736bae029 100644 --- a/tests/e2e/tests/build/environment.ts +++ b/tests/e2e/tests/build/environment.ts @@ -2,13 +2,18 @@ import {ng} from '../../utils/process'; import {expectFileToMatch} from '../../utils/fs'; import {expectGitToBeClean} from '../../utils/git'; import {expectToFail} from '../../utils/utils'; +import {getGlobalVariable} from '../../utils/env'; export default function() { + // Disable parts of it in webpack tests. + const ejected = getGlobalVariable('argv').eject; + // Try a prod build. return ng('build', '--env=prod') .then(() => expectFileToMatch('dist/main.bundle.js', 'production: true')) - .then(() => expectGitToBeClean()) + // If this is an ejected test, the eject will create files so git will not be clean. + .then(() => !ejected && expectGitToBeClean()) // Build fails on invalid build target .then(() => expectToFail(() => ng('build', '--target=potato'))) diff --git a/tests/e2e/tests/build/json.ts b/tests/e2e/tests/build/json.ts index a003122fdda8..dc116f60719a 100644 --- a/tests/e2e/tests/build/json.ts +++ b/tests/e2e/tests/build/json.ts @@ -1,9 +1,15 @@ import {ng} from '../../utils/process'; import {expectFileToExist} from '../../utils/fs'; import {expectGitToBeClean} from '../../utils/git'; +import {getGlobalVariable} from '../../utils/env'; export default function() { + // Skip this in ejected tests. + if (getGlobalVariable('argv').eject) { + return Promise.resolve(); + } + return ng('build', '--stats-json') .then(() => expectFileToExist('./dist/stats.json')) .then(() => expectGitToBeClean()); diff --git a/tests/e2e/tests/build/output-dir.ts b/tests/e2e/tests/build/output-dir.ts index c159fe287398..5273cbd0c558 100644 --- a/tests/e2e/tests/build/output-dir.ts +++ b/tests/e2e/tests/build/output-dir.ts @@ -1,11 +1,17 @@ -import {ng} from '../../utils/process'; +import {getGlobalVariable} from '../../utils/env'; import {expectFileToExist} from '../../utils/fs'; -import {expectToFail} from '../../utils/utils'; import {expectGitToBeClean} from '../../utils/git'; +import {ng} from '../../utils/process'; import {updateJsonFile} from '../../utils/project'; +import {expectToFail} from '../../utils/utils'; export default function() { + // Skip this in ejected tests. + if (getGlobalVariable('argv').eject) { + return Promise.resolve(); + } + return ng('build', '-op', './build-output') .then(() => expectFileToExist('./build-output/index.html')) .then(() => expectFileToExist('./build-output/main.bundle.js')) diff --git a/tests/e2e/tests/build/prod-build.ts b/tests/e2e/tests/build/prod-build.ts index 7a8d5010306f..8b064460d168 100644 --- a/tests/e2e/tests/build/prod-build.ts +++ b/tests/e2e/tests/build/prod-build.ts @@ -3,9 +3,13 @@ import {readdirSync} from 'fs'; import {expectFileToExist, expectFileToMatch} from '../../utils/fs'; import {ng} from '../../utils/process'; import {expectGitToBeClean} from '../../utils/git'; +import {getGlobalVariable} from '../../utils/env'; export default function() { + // Skip this in ejected tests. + const ejected = getGlobalVariable('argv').eject; + // Can't use the `ng` helper because somewhere the environment gets // stuck to the first build done return ng('build', '--prod') @@ -19,5 +23,5 @@ export default function() { expectFileToMatch(`dist/${main}`, /bootstrapModuleFactory\(/); }) // Check that the process didn't change local files. - .then(() => expectGitToBeClean()); + .then(() => !ejected && expectGitToBeClean()); } diff --git a/tests/e2e/tests/build/rebuild-css-change.ts b/tests/e2e/tests/build/rebuild-css-change.ts index 949368653f0d..ccf12b9a2d3a 100644 --- a/tests/e2e/tests/build/rebuild-css-change.ts +++ b/tests/e2e/tests/build/rebuild-css-change.ts @@ -5,6 +5,7 @@ import { silentExecAndWaitForOutputToMatch } from '../../utils/process'; import {appendToFile} from '../../utils/fs'; +import {getGlobalVariable} from '../../utils/env'; const webpackGoodRegEx = /webpack: bundle is now VALID|webpack: Compiled successfully./; @@ -12,6 +13,11 @@ export default function() { if (process.platform.startsWith('win')) { return Promise.resolve(); } + // Skip this in ejected tests. + if (getGlobalVariable('argv').eject) { + return Promise.resolve(); + } + return silentExecAndWaitForOutputToMatch('ng', ['serve'], webpackGoodRegEx) // Should trigger a rebuild. diff --git a/tests/e2e/tests/build/rebuild-deps-type-check.ts b/tests/e2e/tests/build/rebuild-deps-type-check.ts index 55a15ad4b2a3..863bac41a45f 100644 --- a/tests/e2e/tests/build/rebuild-deps-type-check.ts +++ b/tests/e2e/tests/build/rebuild-deps-type-check.ts @@ -5,6 +5,7 @@ import { } from '../../utils/process'; import {writeFile, prependToFile, appendToFile} from '../../utils/fs'; import {wait} from '../../utils/utils'; +import {getGlobalVariable} from '../../utils/env'; const doneRe = @@ -16,6 +17,10 @@ export default function() { if (process.platform.startsWith('win')) { return Promise.resolve(); } + // Skip this in ejected tests. + if (getGlobalVariable('argv').eject) { + return Promise.resolve(); + } return silentExecAndWaitForOutputToMatch('ng', ['serve'], doneRe) // Create and import files. diff --git a/tests/e2e/tests/build/rebuild.ts b/tests/e2e/tests/build/rebuild.ts index 33d64e8a6150..000a7d8aae94 100644 --- a/tests/e2e/tests/build/rebuild.ts +++ b/tests/e2e/tests/build/rebuild.ts @@ -8,12 +8,17 @@ import { import {writeFile, writeMultipleFiles, appendToFile, expectFileToMatch} from '../../utils/fs'; import {wait} from '../../utils/utils'; import {request} from '../../utils/http'; +import {getGlobalVariable} from '../../utils/env'; export default function() { if (process.platform.startsWith('win')) { return Promise.resolve(); } + // Skip this in ejected tests. + if (getGlobalVariable('argv').eject) { + return Promise.resolve(); + } let oldNumberOfChunks = 0; const chunkRegExp = /chunk\s+\{/g; diff --git a/tests/e2e/tests/build/service-worker.ts b/tests/e2e/tests/build/service-worker.ts index 323c7e5299d8..a48b13300772 100644 --- a/tests/e2e/tests/build/service-worker.ts +++ b/tests/e2e/tests/build/service-worker.ts @@ -1,9 +1,14 @@ import {join} from 'path'; -import {npm} from '../../utils/process'; +import {getGlobalVariable} from '../../utils/env'; import {expectFileToExist} from '../../utils/fs'; -import {ng} from '../../utils/process'; +import {ng, npm} from '../../utils/process'; export default function() { + // Skip this in ejected tests. + if (getGlobalVariable('argv').eject) { + return Promise.resolve(); + } + // Can't use the `ng` helper because somewhere the environment gets // stuck to the first build done return npm('install', '@angular/service-worker') diff --git a/tests/e2e/utils/fs.ts b/tests/e2e/utils/fs.ts index 5740c3d28202..4edb62b5a113 100644 --- a/tests/e2e/utils/fs.ts +++ b/tests/e2e/utils/fs.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import * as rimrafPackage from 'rimraf'; import {dirname} from 'path'; import {stripIndents} from 'common-tags'; @@ -41,6 +42,19 @@ export function deleteFile(path: string) { } +export function rimraf(path: string) { + return new Promise((resolve, reject) => { + rimrafPackage(path, (err?: any) => { + if (err) { + reject(err); + } else { + resolve(); + } + }) + } +} + + export function moveFile(from: string, to: string) { return new Promise((resolve, reject) => { fs.rename(from, to, (err) => { diff --git a/tests/e2e_runner.ts b/tests/e2e_runner.ts index 636de16f8068..5e9bb14d2e17 100644 --- a/tests/e2e_runner.ts +++ b/tests/e2e_runner.ts @@ -39,7 +39,7 @@ Error.stackTraceLimit = Infinity; * If unnamed flags are passed in, the list of tests will be filtered to include only those passed. */ const argv = minimist(process.argv.slice(2), { - 'boolean': ['debug', 'nolink', 'nightly', 'noproject', 'verbose'], + 'boolean': ['debug', 'nolink', 'nightly', 'noproject', 'verbose', 'eject'], 'string': ['glob', 'ignore', 'reuse', 'ng-sha', ] }); From 8f2b1cea83c0fe65986f2f8c487f29baf86a6861 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 16 Feb 2017 15:24:37 -0800 Subject: [PATCH 7/8] feat(@angular/cli): version shows ejected app or not And show the CLI config version if its different. --- packages/@angular/cli/commands/version.ts | 25 +++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/@angular/cli/commands/version.ts b/packages/@angular/cli/commands/version.ts index 73b445a229fd..91ad68a35416 100644 --- a/packages/@angular/cli/commands/version.ts +++ b/packages/@angular/cli/commands/version.ts @@ -2,6 +2,8 @@ const Command = require('../ember-cli/lib/models/command'); import * as path from 'path'; import * as child_process from 'child_process'; import * as chalk from 'chalk'; +import { CliConfig } from '../models/config'; + const VersionCommand = Command.extend({ name: 'version', @@ -40,6 +42,13 @@ const VersionCommand = Command.extend({ ngCliVersion = `local (v${pkg.version}, branch: ${gitBranch})`; } + const config = CliConfig.fromProject(); + if (config && config.config.project.version !== pkg.version) { + ngCliVersion += ` [${config.config.project.version}]`; + } + if (config && config.config.project.ejected) { + ngCliVersion += ' (e)'; + } if (projPkg) { roots.forEach(root => { @@ -76,12 +85,16 @@ const VersionCommand = Command.extend({ }, getVersion: function(moduleName: string): string { - const modulePkg = require(path.resolve( - this.project.root, - 'node_modules', - moduleName, - 'package.json')); - return modulePkg.version; + try { + const modulePkg = require(path.resolve( + this.project.root, + 'node_modules', + moduleName, + 'package.json')); + return modulePkg.version; + } catch (e) { + return 'error'; + } }, printVersion: function (module: string, version: string) { From 596f35df2e2569fbc01b4b004ab20dcb1853ce95 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 16 Feb 2017 15:58:55 -0800 Subject: [PATCH 8/8] feat(@angular/cli): disable invalid commands on ejected apps --- packages/@angular/cli/tasks/build.ts | 8 ++++++-- packages/@angular/cli/tasks/e2e.ts | 8 ++++++++ packages/@angular/cli/tasks/serve.ts | 3 +++ packages/@angular/cli/tasks/test.ts | 15 +++++++++++++-- tests/e2e/tests/commands/eject/eject.ts | 17 +++++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/tests/commands/eject/eject.ts diff --git a/packages/@angular/cli/tasks/build.ts b/packages/@angular/cli/tasks/build.ts index 4bb9773e1a23..30af991b0b9d 100644 --- a/packages/@angular/cli/tasks/build.ts +++ b/packages/@angular/cli/tasks/build.ts @@ -14,10 +14,14 @@ export default Task.extend({ run: function (runTaskOptions: BuildTaskOptions) { const project = this.cliProject; + const config = CliConfig.fromProject().config; - const outputPath = runTaskOptions.outputPath || CliConfig.fromProject().config.apps[0].outDir; + const outputPath = runTaskOptions.outputPath || config.apps[0].outDir; if (project.root === outputPath) { - throw new SilentError ('Output path MUST not be project root directory!'); + throw new SilentError('Output path MUST not be project root directory!'); + } + if (config.project && config.project.ejected) { + throw new SilentError('An ejected project cannot use the build command anymore.'); } rimraf.sync(path.resolve(project.root, outputPath)); diff --git a/packages/@angular/cli/tasks/e2e.ts b/packages/@angular/cli/tasks/e2e.ts index ffbc335f851b..824c245cd7a5 100644 --- a/packages/@angular/cli/tasks/e2e.ts +++ b/packages/@angular/cli/tasks/e2e.ts @@ -1,15 +1,23 @@ import * as url from 'url'; import { E2eTaskOptions } from '../commands/e2e'; +import { CliConfig } from '../models/config'; import { requireProjectModule } from '../utilities/require-project-module'; + const Task = require('../ember-cli/lib/models/task'); +const SilentError = require('silent-error'); export const E2eTask = Task.extend({ run: function (e2eTaskOptions: E2eTaskOptions) { + const projectConfig = CliConfig.fromProject().config; const projectRoot = this.project.root; const protractorLauncher = requireProjectModule(projectRoot, 'protractor/built/launcher'); + if (projectConfig.project && projectConfig.project.ejected) { + throw new SilentError('An ejected project cannot use the build command anymore.'); + } + return new Promise(function () { let promise = Promise.resolve(); let additionalProtractorConfig: any = { diff --git a/packages/@angular/cli/tasks/serve.ts b/packages/@angular/cli/tasks/serve.ts index df6c62b2a041..6cd7c3809448 100644 --- a/packages/@angular/cli/tasks/serve.ts +++ b/packages/@angular/cli/tasks/serve.ts @@ -27,6 +27,9 @@ export default Task.extend({ if (this.project.root === outputPath) { throw new SilentError('Output path MUST not be project root directory!'); } + if (projectConfig.project && projectConfig.project.ejected) { + throw new SilentError('An ejected project cannot use the build command anymore.'); + } rimraf.sync(path.resolve(this.project.root, outputPath)); const serveDefaults = { diff --git a/packages/@angular/cli/tasks/test.ts b/packages/@angular/cli/tasks/test.ts index 085f388c3f34..6d987cfb605c 100644 --- a/packages/@angular/cli/tasks/test.ts +++ b/packages/@angular/cli/tasks/test.ts @@ -1,11 +1,22 @@ -const Task = require('../ember-cli/lib/models/task'); -import { TestOptions } from '../commands/test'; import * as path from 'path'; + +import { TestOptions } from '../commands/test'; +import { CliConfig } from '../models/config'; import { requireProjectModule } from '../utilities/require-project-module'; +const Task = require('../ember-cli/lib/models/task'); +const SilentError = require('silent-error'); + + export default Task.extend({ run: function (options: TestOptions) { + const projectConfig = CliConfig.fromProject().config; const projectRoot = this.project.root; + + if (projectConfig.project && projectConfig.project.ejected) { + throw new SilentError('An ejected project cannot use the build command anymore.'); + } + return new Promise((resolve) => { const karma = requireProjectModule(projectRoot, 'karma'); const karmaConfig = path.join(projectRoot, options.config || diff --git a/tests/e2e/tests/commands/eject/eject.ts b/tests/e2e/tests/commands/eject/eject.ts new file mode 100644 index 000000000000..0205f675cf85 --- /dev/null +++ b/tests/e2e/tests/commands/eject/eject.ts @@ -0,0 +1,17 @@ +import * as path from 'path'; + +import {ng, silentNpm, exec} from '../../../utils/process'; +import {expectToFail} from '../../../utils/utils'; +import {expectGitToBeClean} from '../../../utils/git'; + + +export default function() { + return ng('eject') + .then(() => expectToFail(() => ng('build'))) + .then(() => expectToFail(() => ng('test'))) + .then(() => expectToFail(() => ng('e2e'))) + .then(() => expectToFail(() => ng('serve'))) + .then(() => expectToFail(() => expectGitToBeClean())) + .then(() => silentNpm('install')) + .then(() => exec(path.join('node_modules', '.bin', 'webpack'))); +}