import { readFile, writeFile, replaceInFile } from './fs'; import { execAndWaitForOutputToMatch, npm, silentNpm, ng } from './process'; import { getGlobalVariable } from './env'; const packages = require('../../../../lib/packages').packages; const tsConfigPath = 'tsconfig.json'; export function updateJsonFile(filePath: string, fn: (json: any) => any | void) { return readFile(filePath) .then(tsConfigJson => { const tsConfig = JSON.parse(tsConfigJson); const result = fn(tsConfig) || tsConfig; return writeFile(filePath, JSON.stringify(result, null, 2)); }); } export function updateTsConfig(fn: (json: any) => any | void) { return updateJsonFile(tsConfigPath, fn); } export function ngServe(...args: string[]) { return execAndWaitForOutputToMatch('ng', ['serve', ...args], /: Compiled successfully./); } export function createProject(name: string, ...args: string[]) { const argv: any = getGlobalVariable('argv'); return Promise.resolve() .then(() => process.chdir(getGlobalVariable('tmp-root'))) .then(() => ng('new', name, '--skip-install', ...args)) .then(() => process.chdir(name)) .then(() => useBuiltPackages()) .then(() => useCIChrome('e2e')) .then(() => useCIChrome('src')) .then(() => useDevKitSnapshots()) .then(() => argv['ng2'] ? useNg2() : Promise.resolve()) .then(() => argv['ng4'] ? useNg4() : Promise.resolve()) .then(() => argv['ng-snapshots'] || argv['ng-tag'] ? useSha() : Promise.resolve()) .then(() => console.log(`Project ${name} created... Installing npm.`)) .then(() => silentNpm('install')) .then(() => useCIDefaults(name)); } export function useDevKit(devkitRoot: string) { return Promise.resolve() .then(() => { // Load the packages info for devkit. const devkitPackages = require(devkitRoot + '/lib/packages').packages; return updateJsonFile('package.json', json => { if (!json['dependencies']) { json['dependencies'] = {}; } if (!json['devDependencies']) { json['devDependencies'] = {}; } for (const packageName of Object.keys(devkitPackages)) { if (json['dependencies'].hasOwnProperty(packageName)) { json['dependencies'][packageName] = devkitPackages[packageName].tar; } else if (json['devDependencies'].hasOwnProperty(packageName)) { json['devDependencies'][packageName] = devkitPackages[packageName].tar; } } }); }); } export function useDevKitSnapshots() { return updateJsonFile('package.json', json => { // TODO: actually add these. // These were not working on any test that ran `npm i`. // json['devDependencies']['@angular-devkit/build-angular'] = // 'github:angular/angular-devkit-build-angular-builds'; // // By adding build-ng-packagr preemptively, adding a lib will not update it. // json['devDependencies']['@angular-devkit/build-ng-packagr'] = // 'github:angular/angular-devkit-build-ng-packagr-builds'; }); } export function useBuiltPackages() { return Promise.resolve() .then(() => updateJsonFile('package.json', json => { if (!json['dependencies']) { json['dependencies'] = {}; } if (!json['devDependencies']) { json['devDependencies'] = {}; } for (const packageName of Object.keys(packages)) { if (json['dependencies'].hasOwnProperty(packageName)) { json['dependencies'][packageName] = packages[packageName].tar; } else if (json['devDependencies'].hasOwnProperty(packageName)) { json['devDependencies'][packageName] = packages[packageName].tar; } } })); } export function useSha() { const argv = getGlobalVariable('argv'); if (argv['ng-snapshots'] || argv['ng-tag']) { // We need more than the sha here, version is also needed. Examples of latest tags: // 7.0.0-beta.4+dd2a650 // 6.1.6+4a8d56a const label = argv['ng-tag'] ? argv['ng-tag'] : ''; return updateJsonFile('package.json', json => { // Install over the project with snapshot builds. Object.keys(json['dependencies'] || {}) .filter(name => name.match(/^@angular\//)) .forEach(name => { const pkgName = name.split(/\//)[1]; if (pkgName == 'cli') { return; } json['dependencies'][`@angular/${pkgName}`] = `github:angular/${pkgName}-builds${label}`; }); Object.keys(json['devDependencies'] || {}) .filter(name => name.match(/^@angular\//)) .forEach(name => { const pkgName = name.split(/\//)[1]; if (pkgName == 'cli') { return; } json['devDependencies'][`@angular/${pkgName}`] = `github:angular/${pkgName}-builds${label}`; }); json['devDependencies']['typescript'] = '~3.0.1'; }); } else { return Promise.resolve(); } } export function useNgVersion(version: string) { return updateJsonFile('package.json', json => { // Install over the project with specific versions. Object.keys(json['dependencies'] || {}) .filter(name => name.match(/^@angular\//)) .forEach(name => { const pkgName = name.split(/\//)[1]; if (pkgName == 'cli') { return; } json['dependencies'][`@angular/${pkgName}`] = version; }); Object.keys(json['devDependencies'] || {}) .filter(name => name.match(/^@angular\//)) .forEach(name => { const pkgName = name.split(/\//)[1]; if (pkgName == 'cli') { return; } json['devDependencies'][`@angular/${pkgName}`] = version; }); // TODO: determine the appropriate version for the Angular version if (version.startsWith('^5')) { json['devDependencies']['typescript'] = '~2.5.0'; json['dependencies']['rxjs'] = '5.5.8'; } else { json['devDependencies']['typescript'] = '~2.7.0'; json['dependencies']['rxjs'] = '6.0.0-rc.0'; } }); } export function useCIDefaults(projectName = 'test-project') { return updateJsonFile('angular.json', workspaceJson => { // Disable progress reporting on CI to reduce spam. const appTargets = workspaceJson.projects[projectName].targets; appTargets.build.options.progress = false; appTargets.test.options.progress = false; // Disable auto-updating webdriver in e2e. const e2eTargets = workspaceJson.projects[projectName + '-e2e'].targets; e2eTargets.e2e.options.webdriverUpdate = false; }) .then(() => updateJsonFile('package.json', json => { // We want to always use the same version of webdriver but can only do so on CircleCI. // Appveyor and Travis will use latest Chrome stable. // CircleCI (via ngcontainer:0.1.1) uses Chrome 63.0.3239.84. // Appveyor (via chocolatey) cannot use older versions of Chrome at all: // https://github.com/chocolatey/chocolatey-coreteampackages/tree/master/automatic/googlechrome // webdriver 2.33 matches Chrome 63.0.3239.84. // webdriver 2.37 matches Chrome 65.0.3325.18100 (latest stable). // The webdriver versions for latest stable will need to be manually updated. const webdriverVersion = process.env['CIRCLECI'] ? '2.33' : '2.37'; const driverOption = process.env['CHROMEDRIVER_VERSION_ARG'] || `--versions.chrome ${webdriverVersion}`; json['scripts']['webdriver-update'] = 'webdriver-manager update' + ` --standalone false --gecko false ${driverOption}`; })) .then(() => npm('run', 'webdriver-update')); } export function useCIChrome(projectDir: string) { // There's a race condition happening in Chrome. Enabling logging in chrome used by // protractor actually fixes it. Logging is piped to a file so it doesn't affect our setup. // --no-sandbox is needed for Circle CI. // Travis can use headless chrome, but not appveyor. return Promise.resolve() .then(() => replaceInFile(`${projectDir}/protractor.conf.js`, `'browserName': 'chrome'`, `'browserName': 'chrome', chromeOptions: { args: [ "--enable-logging", // "--no-sandbox", // "--headless" ] } `)) // Not a problem if the file can't be found. // .catch(() => null) // .then(() => replaceInFile(`${projectDir}/karma.conf.js`, `browsers: ['Chrome'],`, // `browsers: ['ChromeCI'], // customLaunchers: { // ChromeCI: { // base: 'ChromeHeadless', // flags: ['--no-sandbox'] // } // }, // `)) .catch(() => null); } // Convert a Angular 5 project to Angular 2. export function useNg2() { const ng2Deps: any = { 'dependencies': { '@angular/common': '^2.4.0', '@angular/compiler': '^2.4.0', '@angular/core': '^2.4.0', '@angular/forms': '^2.4.0', '@angular/http': '^2.4.0', '@angular/platform-browser': '^2.4.0', '@angular/platform-browser-dynamic': '^2.4.0', '@angular/router': '^3.4.0', 'zone.js': '^0.7.4' }, 'devDependencies': { '@angular/compiler-cli': '^2.4.0', '@types/jasmine': '~2.2.0', '@types/jasminewd2': undefined, 'typescript': '~2.0.0' } }; const tsconfigAppJson: any = { 'compilerOptions': { 'sourceMap': true, 'declaration': false, 'moduleResolution': 'node', 'emitDecoratorMetadata': true, 'experimentalDecorators': true, 'target': 'es5', 'lib': [ 'es2017', 'dom' ], 'outDir': '../out-tsc/app', 'module': 'es2015', 'baseUrl': '', 'types': [] }, 'exclude': [ 'test.ts', '**/*.spec.ts' ] }; const tsconfigSpecJson: any = { 'compilerOptions': { 'sourceMap': true, 'declaration': false, 'moduleResolution': 'node', 'emitDecoratorMetadata': true, 'experimentalDecorators': true, 'lib': [ 'es2017', 'dom' ], 'outDir': '../out-tsc/spec', 'module': 'commonjs', 'target': 'es5', 'baseUrl': '', 'types': [ 'jasmine', 'node' ] }, 'files': [ 'test.ts' ], 'include': [ '**/*.spec.ts', '**/*.d.ts' ] }; const tsconfigE2eJson: any = { 'compilerOptions': { 'sourceMap': true, 'declaration': false, 'moduleResolution': 'node', 'emitDecoratorMetadata': true, 'experimentalDecorators': true, 'lib': [ 'es2017' ], 'outDir': '../out-tsc/e2e', 'module': 'commonjs', 'target': 'es5', 'types': [ 'jasmine', 'node' ] } }; return Promise.resolve() .then(() => updateJsonFile('package.json', json => { Object.keys(ng2Deps['dependencies']).forEach(pkgName => { json['dependencies'][pkgName] = ng2Deps['dependencies'][pkgName]; }); Object.keys(ng2Deps['devDependencies']).forEach(pkgName => { json['devDependencies'][pkgName] = ng2Deps['devDependencies'][pkgName]; }); console.log(JSON.stringify(json)); })) .then(() => updateJsonFile('src/tsconfig.app.json', json => Object.assign(json, tsconfigAppJson))) .then(() => updateJsonFile('src/tsconfig.spec.json', json => Object.assign(json, tsconfigSpecJson))) .then(() => updateJsonFile('e2e/tsconfig.e2e.json', json => Object.assign(json, tsconfigE2eJson))) .then(() => replaceInFile('src/test.ts', 'import \'zone.js/dist/zone-testing\';', ` import 'zone.js/dist/long-stack-trace-zone'; import 'zone.js/dist/proxy.js'; import 'zone.js/dist/sync-test'; import 'zone.js/dist/jasmine-patch'; import 'zone.js/dist/async-test'; import 'zone.js/dist/fake-async-test'; `)); } // Convert a Angular 5 project to Angular 4. export function useNg4() { const ng4Deps: any = { 'dependencies': { '@angular/common': '^4.4.6', '@angular/compiler': '^4.4.6', '@angular/core': '^4.4.6', '@angular/forms': '^4.4.6', '@angular/http': '^4.4.6', '@angular/platform-browser': '^4.4.6', '@angular/platform-browser-dynamic': '^4.4.6', '@angular/router': '^4.4.6', 'zone.js': '^0.8.14' }, 'devDependencies': { '@angular/compiler-cli': '^4.4.6', 'typescript': '~2.3.3' } }; return Promise.resolve() .then(() => updateJsonFile('package.json', json => { Object.keys(ng4Deps['dependencies']).forEach(pkgName => { json['dependencies'][pkgName] = ng4Deps['dependencies'][pkgName]; }); Object.keys(ng4Deps['devDependencies']).forEach(pkgName => { json['devDependencies'][pkgName] = ng4Deps['devDependencies'][pkgName]; }); console.log(JSON.stringify(json)); })); }