"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ const core_1 = require("@angular-devkit/core"); const schematics_1 = require("@angular-devkit/schematics"); const tasks_1 = require("@angular-devkit/schematics/tasks"); const dependencies_1 = require("../../utility/dependencies"); const json_utils_1 = require("../../utility/json-utils"); const latest_versions_1 = require("../../utility/latest-versions"); const defaults = { appRoot: 'src', index: 'index.html', main: 'main.ts', polyfills: 'polyfills.ts', tsConfig: 'tsconfig.app.json', test: 'test.ts', outDir: 'dist/', karma: 'karma.conf.js', protractor: 'protractor.conf.js', testTsConfig: 'tsconfig.spec.json', serverOutDir: 'dist-server', serverMain: 'main.server.ts', serverTsConfig: 'tsconfig.server.json', }; function getConfigPath(tree) { let possiblePath = core_1.normalize('.angular-cli.json'); if (tree.exists(possiblePath)) { return possiblePath; } possiblePath = core_1.normalize('angular-cli.json'); if (tree.exists(possiblePath)) { return possiblePath; } throw new schematics_1.SchematicsException('Could not find configuration file'); } function migrateKarmaConfiguration(config) { return (host, context) => { context.logger.info(`Updating karma configuration`); try { const karmaPath = config && config.test && config.test.karma && config.test.karma.config ? config.test.karma.config : defaults.karma; const buffer = host.read(karmaPath); if (buffer !== null) { let content = buffer.toString(); // Replace the 1.0 files and preprocessor entries, with and without comma at the end. // If these remain, they will cause the `ng test` to fail. content = content.replace(`{ pattern: './src/test.ts', watched: false },`, ''); content = content.replace(`{ pattern: './src/test.ts', watched: false }`, ''); content = content.replace(`'./src/test.ts': ['@angular/cli'],`, ''); content = content.replace(`'./src/test.ts': ['@angular/cli']`, ''); content = content.replace(/angularCli[^}]*},?/, ''); // Replace 1.x plugin names. content = content.replace(/@angular\/cli/g, '@angular-devkit/build-angular'); // Replace code coverage output path. content = content.replace('reports', `dir: require('path').join(__dirname, 'coverage'), reports`); host.overwrite(karmaPath, content); } } catch (_a) { } return host; }; } function migrateConfiguration(oldConfig, logger) { return (host, context) => { const oldConfigPath = getConfigPath(host); const configPath = core_1.normalize('angular.json'); context.logger.info(`Updating configuration`); const config = { '$schema': './node_modules/@angular/cli/lib/config/schema.json', version: 1, newProjectRoot: 'projects', projects: extractProjectsConfig(oldConfig, host, logger), }; const defaultProject = extractDefaultProject(oldConfig); if (defaultProject !== null) { config.defaultProject = defaultProject; } const cliConfig = extractCliConfig(oldConfig); if (cliConfig !== null) { config.cli = cliConfig; } const schematicsConfig = extractSchematicsConfig(oldConfig); if (schematicsConfig !== null) { config.schematics = schematicsConfig; } const targetsConfig = extractTargetsConfig(oldConfig); if (targetsConfig !== null) { config.architect = targetsConfig; } context.logger.info(`Removing old config file (${oldConfigPath})`); host.delete(oldConfigPath); context.logger.info(`Writing config file (${configPath})`); host.create(configPath, JSON.stringify(config, null, 2)); return host; }; } function extractCliConfig(config) { const newConfig = {}; if (config.packageManager && config.packageManager !== 'default') { newConfig['packageManager'] = config.packageManager; } if (config.warnings) { if (config.warnings.versionMismatch !== undefined) { newConfig.warnings = { ...(newConfig.warnings || {}), ...{ versionMismatch: config.warnings.versionMismatch }, }; } } return Object.getOwnPropertyNames(newConfig).length == 0 ? null : newConfig; } function extractSchematicsConfig(config) { let collectionName = '@schematics/angular'; if (!config || !config.defaults) { return null; } // const configDefaults = config.defaults; if (config.defaults && config.defaults.schematics && config.defaults.schematics.collection) { collectionName = config.defaults.schematics.collection; } /** * For each schematic * - get the config * - filter one's without config * - combine them into an object */ // tslint:disable-next-line:no-any const schematicConfigs = ['class', 'component', 'directive', 'guard', 'interface', 'module', 'pipe', 'service'] .map(schematicName => { // tslint:disable-next-line:no-any const schematicDefaults = config.defaults[schematicName] || null; return { schematicName, config: schematicDefaults, }; }) .filter(schematic => schematic.config !== null) .reduce((all, schematic) => { all[collectionName + ':' + schematic.schematicName] = schematic.config; return all; }, {}); const componentUpdate = {}; componentUpdate.prefix = ''; const componentKey = collectionName + ':component'; const directiveKey = collectionName + ':directive'; if (!schematicConfigs[componentKey]) { schematicConfigs[componentKey] = {}; } if (!schematicConfigs[directiveKey]) { schematicConfigs[directiveKey] = {}; } if (config.apps && config.apps[0]) { schematicConfigs[componentKey].prefix = config.apps[0].prefix; schematicConfigs[directiveKey].prefix = config.apps[0].prefix; } if (config.defaults) { schematicConfigs[componentKey].styleext = config.defaults.styleExt; } return schematicConfigs; } function extractTargetsConfig(_config) { return null; } // This function is too big, but also really hard to refactor properly as the whole config // depends on all parts of the config. // tslint:disable-next-line:no-big-function function extractProjectsConfig(config, tree, logger) { const builderPackage = '@angular-devkit/build-angular'; const defaultAppNamePrefix = getDefaultAppNamePrefix(config); const buildDefaults = config.defaults && config.defaults.build ? { sourceMap: config.defaults.build.sourcemaps, progress: config.defaults.build.progress, poll: config.defaults.build.poll, deleteOutputPath: config.defaults.build.deleteOutputPath, preserveSymlinks: config.defaults.build.preserveSymlinks, showCircularDependencies: config.defaults.build.showCircularDependencies, commonChunk: config.defaults.build.commonChunk, namedChunks: config.defaults.build.namedChunks, } : {}; const serveDefaults = config.defaults && config.defaults.serve ? { port: config.defaults.serve.port, host: config.defaults.serve.host, ssl: config.defaults.serve.ssl, sslKey: config.defaults.serve.sslKey, sslCert: config.defaults.serve.sslCert, proxyConfig: config.defaults.serve.proxyConfig, } : {}; const apps = config.apps || []; // convert the apps to projects const browserApps = apps.filter(app => app.platform !== 'server'); const serverApps = apps.filter(app => app.platform === 'server'); const projectMap = browserApps // This function is too big, but also really hard to refactor properly as the whole config // depends on all parts of the config. // tslint:disable-next-line:no-big-function .map((app, idx) => { const defaultAppName = idx === 0 ? defaultAppNamePrefix : `${defaultAppNamePrefix}${idx}`; const name = app.name || defaultAppName; const outDir = app.outDir || defaults.outDir; const appRoot = app.root || defaults.appRoot; function _mapAssets(asset) { if (typeof asset === 'string') { return core_1.normalize(appRoot + '/' + asset); } else { if (asset.allowOutsideOutDir) { logger.warn(core_1.tags.oneLine ` Asset with input '${asset.input}' was not migrated because it uses the 'allowOutsideOutDir' option which is not supported in Angular CLI 6. `); return null; } else if (asset.output) { return { glob: asset.glob, input: core_1.normalize(appRoot + '/' + asset.input), output: core_1.normalize('/' + asset.output), }; } else { return { glob: asset.glob, input: core_1.normalize(appRoot + '/' + asset.input), output: '/', }; } } } function _buildConfigurations() { const source = app.environmentSource; const environments = app.environments; const serviceWorker = app.serviceWorker; const productionPartial = { optimization: true, outputHashing: 'all', sourceMap: false, extractCss: true, namedChunks: false, aot: true, extractLicenses: true, vendorChunk: false, buildOptimizer: true, ...(serviceWorker ? { serviceWorker: true, ngswConfigPath: 'src/ngsw-config.json' } : {}), ...(app.budgets ? { budgets: app.budgets } : {}), }; if (!environments) { return { production: productionPartial }; } const configurations = Object.keys(environments).reduce((acc, environment) => { if (source === environments[environment]) { return acc; } let isProduction = false; const environmentContent = tree.read(app.root + '/' + environments[environment]); if (environmentContent) { isProduction = !!environmentContent.toString('utf-8') // Allow for `production: true` or `production = true`. Best we can do to guess. .match(/production['"]?\s*[:=]\s*true/); } let configurationName; // We used to use `prod` by default as the key, instead we now use the full word. // Try not to override the production key if it's there. if (environment == 'prod' && !environments['production'] && isProduction) { configurationName = 'production'; } else { configurationName = environment; } acc[configurationName] = { ...(isProduction ? productionPartial : {}), fileReplacements: [ { replace: `${app.root}/${source}`, with: `${app.root}/${environments[environment]}`, }, ], }; return acc; }, {}); if (!configurations['production']) { configurations['production'] = { ...productionPartial }; } return configurations; } function _serveConfigurations() { const environments = app.environments; if (!environments) { return {}; } if (!targets) { throw new Error(); } const configurations = targets.build.configurations; return Object.keys(configurations).reduce((acc, environment) => { acc[environment] = { browserTarget: `${name}:build:${environment}` }; return acc; }, {}); } function _extraEntryMapper(extraEntry) { let entry; if (typeof extraEntry === 'string') { entry = core_1.join(app.root, extraEntry); } else { const input = core_1.join(app.root, extraEntry.input || ''); entry = { input, lazy: extraEntry.lazy }; if (extraEntry.output) { entry.bundleName = extraEntry.output; } } return entry; } const projectRoot = core_1.join(core_1.normalize(appRoot), '..'); const project = { root: projectRoot, sourceRoot: appRoot, projectType: 'application', }; const targets = {}; project.architect = targets; // Browser target const buildOptions = { // Make outputPath relative to root. outputPath: outDir, index: `${appRoot}/${app.index || defaults.index}`, main: `${appRoot}/${app.main || defaults.main}`, tsConfig: `${appRoot}/${app.tsconfig || defaults.tsConfig}`, ...(app.baseHref ? { baseHref: app.baseHref } : {}), ...buildDefaults, }; if (app.polyfills) { buildOptions.polyfills = appRoot + '/' + app.polyfills; } if (app.stylePreprocessorOptions && app.stylePreprocessorOptions.includePaths && Array.isArray(app.stylePreprocessorOptions.includePaths) && app.stylePreprocessorOptions.includePaths.length > 0) { buildOptions.stylePreprocessorOptions = { includePaths: app.stylePreprocessorOptions.includePaths .map(includePath => core_1.join(app.root, includePath)), }; } buildOptions.assets = (app.assets || []).map(_mapAssets).filter(x => !!x); buildOptions.styles = (app.styles || []).map(_extraEntryMapper); buildOptions.scripts = (app.scripts || []).map(_extraEntryMapper); targets.build = { builder: `${builderPackage}:browser`, options: buildOptions, configurations: _buildConfigurations(), }; // Serve target const serveOptions = { browserTarget: `${name}:build`, ...serveDefaults, }; targets.serve = { builder: `${builderPackage}:dev-server`, options: serveOptions, configurations: _serveConfigurations(), }; // Extract target const extractI18nOptions = { browserTarget: `${name}:build` }; targets['extract-i18n'] = { builder: `${builderPackage}:extract-i18n`, options: extractI18nOptions, }; const karmaConfig = config.test && config.test.karma ? config.test.karma.config || '' : ''; // Test target const testOptions = { main: appRoot + '/' + app.test || defaults.test, // Make karmaConfig relative to root. karmaConfig, }; if (app.polyfills) { testOptions.polyfills = appRoot + '/' + app.polyfills; } if (app.testTsconfig) { testOptions.tsConfig = appRoot + '/' + app.testTsconfig; } const codeCoverageExclude = config.test && config.test.codeCoverage && config.test.codeCoverage.exclude; if (codeCoverageExclude) { testOptions.codeCoverageExclude = codeCoverageExclude; } testOptions.scripts = (app.scripts || []).map(_extraEntryMapper); testOptions.styles = (app.styles || []).map(_extraEntryMapper); testOptions.assets = (app.assets || []).map(_mapAssets).filter(x => !!x); if (karmaConfig) { targets.test = { builder: `${builderPackage}:karma`, options: testOptions, }; } const tsConfigs = []; const excludes = []; let warnForLint = false; if (config && config.lint && Array.isArray(config.lint)) { config.lint.forEach(lint => { if (lint.project) { tsConfigs.push(lint.project); } else { warnForLint = true; } if (lint.exclude) { if (typeof lint.exclude === 'string') { excludes.push(lint.exclude); } else { lint.exclude.forEach(ex => excludes.push(ex)); } } }); } if (warnForLint) { logger.warn(` Lint without 'project' was not migrated which is not supported in Angular CLI 6. `); } const removeDupes = (items) => items.reduce((newItems, item) => { if (newItems.indexOf(item) === -1) { newItems.push(item); } return newItems; }, []); // Tslint target const lintOptions = { tsConfig: removeDupes(tsConfigs).filter(t => t.indexOf('e2e') === -1), exclude: removeDupes(excludes), }; targets.lint = { builder: `${builderPackage}:tslint`, options: lintOptions, }; // server target const serverApp = serverApps .filter(serverApp => app.root === serverApp.root && app.index === serverApp.index)[0]; if (serverApp) { const serverOptions = { outputPath: serverApp.outDir || defaults.serverOutDir, main: `${appRoot}/${serverApp.main || defaults.serverMain}`, tsConfig: `${appRoot}/${serverApp.tsconfig || defaults.serverTsConfig}`, }; const serverTarget = { builder: '@angular-devkit/build-angular:server', options: serverOptions, }; targets.server = serverTarget; } const e2eProject = { root: core_1.join(projectRoot, 'e2e'), sourceRoot: core_1.join(projectRoot, 'e2e'), projectType: 'application', }; const e2eTargets = {}; const protractorConfig = config && config.e2e && config.e2e.protractor && config.e2e.protractor.config ? config.e2e.protractor.config : ''; const e2eOptions = { protractorConfig: protractorConfig, devServerTarget: `${name}:serve`, }; const e2eTarget = { builder: `${builderPackage}:protractor`, options: e2eOptions, }; e2eTargets.e2e = e2eTarget; const e2eLintOptions = { tsConfig: removeDupes(tsConfigs).filter(t => t.indexOf('e2e') !== -1), exclude: removeDupes(excludes), }; const e2eLintTarget = { builder: `${builderPackage}:tslint`, options: e2eLintOptions, }; e2eTargets.lint = e2eLintTarget; if (protractorConfig) { e2eProject.architect = e2eTargets; } return { name, project, e2eProject }; }) .reduce((projects, mappedApp) => { const { name, project, e2eProject } = mappedApp; projects[name] = project; projects[name + '-e2e'] = e2eProject; return projects; }, {}); return projectMap; } function getDefaultAppNamePrefix(config) { let defaultAppNamePrefix = 'app'; if (config.project && config.project.name) { defaultAppNamePrefix = config.project.name; } return defaultAppNamePrefix; } function extractDefaultProject(config) { if (config.apps && config.apps[0]) { const app = config.apps[0]; const defaultAppName = getDefaultAppNamePrefix(config); const name = app.name || defaultAppName; return name; } return null; } function updateSpecTsConfig(config) { return (host, context) => { const apps = config.apps || []; apps.forEach((app, idx) => { const testTsConfig = app.testTsconfig || defaults.testTsConfig; const tsSpecConfigPath = core_1.join(core_1.normalize(app.root || ''), testTsConfig); const buffer = host.read(tsSpecConfigPath); if (!buffer) { return; } const tsCfgAst = core_1.parseJsonAst(buffer.toString(), core_1.JsonParseMode.Loose); if (tsCfgAst.kind != 'object') { throw new schematics_1.SchematicsException('Invalid tsconfig. Was expecting an object'); } const filesAstNode = json_utils_1.findPropertyInAstObject(tsCfgAst, 'files'); if (filesAstNode && filesAstNode.kind != 'array') { throw new schematics_1.SchematicsException('Invalid tsconfig "files" property; expected an array.'); } const recorder = host.beginUpdate(tsSpecConfigPath); const polyfills = app.polyfills || defaults.polyfills; if (!filesAstNode) { // Do nothing if the files array does not exist. This means exclude or include are // set and we shouldn't mess with that. } else { if (filesAstNode.value.indexOf(polyfills) == -1) { json_utils_1.appendValueInAstArray(recorder, filesAstNode, polyfills); } } host.commitUpdate(recorder); }); }; } function updatePackageJson(config) { return (host, context) => { const dependency = { type: dependencies_1.NodeDependencyType.Dev, name: '@angular-devkit/build-angular', version: latest_versions_1.latestVersions.DevkitBuildAngular, overwrite: true, }; dependencies_1.addPackageJsonDependency(host, dependency); context.addTask(new tasks_1.NodePackageInstallTask({ packageManager: config.packageManager === 'default' ? undefined : config.packageManager, })); return host; }; } function updateTsLintConfig() { return (host, context) => { const tsLintPath = '/tslint.json'; const buffer = host.read(tsLintPath); if (!buffer) { return host; } const tsCfgAst = core_1.parseJsonAst(buffer.toString(), core_1.JsonParseMode.Loose); if (tsCfgAst.kind != 'object') { return host; } const rulesNode = json_utils_1.findPropertyInAstObject(tsCfgAst, 'rules'); if (!rulesNode || rulesNode.kind != 'object') { return host; } const importBlacklistNode = json_utils_1.findPropertyInAstObject(rulesNode, 'import-blacklist'); if (!importBlacklistNode || importBlacklistNode.kind != 'array') { return host; } const recorder = host.beginUpdate(tsLintPath); for (let i = 0; i < importBlacklistNode.elements.length; i++) { const element = importBlacklistNode.elements[i]; if (element.kind == 'string' && element.value == 'rxjs') { const { start, end } = element; // Remove this element. if (i == importBlacklistNode.elements.length - 1) { // Last element. if (i > 0) { // Not first, there's a comma to remove before. const previous = importBlacklistNode.elements[i - 1]; recorder.remove(previous.end.offset, end.offset - previous.end.offset); } else { // Only element, just remove the whole rule. const { start, end } = importBlacklistNode; recorder.remove(start.offset, end.offset - start.offset); recorder.insertLeft(start.offset, '[]'); } } else { // Middle, just remove the whole node (up to next node start). const next = importBlacklistNode.elements[i + 1]; recorder.remove(start.offset, next.start.offset - start.offset); } } } host.commitUpdate(recorder); return host; }; } function updateRootTsConfig() { return (host, context) => { const tsConfigPath = '/tsconfig.json'; const buffer = host.read(tsConfigPath); if (!buffer) { return; } const tsCfgAst = core_1.parseJsonAst(buffer.toString(), core_1.JsonParseMode.Loose); if (tsCfgAst.kind !== 'object') { throw new schematics_1.SchematicsException('Invalid root tsconfig. Was expecting an object'); } const compilerOptionsAstNode = json_utils_1.findPropertyInAstObject(tsCfgAst, 'compilerOptions'); if (!compilerOptionsAstNode || compilerOptionsAstNode.kind != 'object') { throw new schematics_1.SchematicsException('Invalid root tsconfig "compilerOptions" property; expected an object.'); } if (json_utils_1.findPropertyInAstObject(compilerOptionsAstNode, 'baseUrl') && json_utils_1.findPropertyInAstObject(compilerOptionsAstNode, 'module')) { return host; } const compilerOptions = compilerOptionsAstNode.value; const { baseUrl = './', module = 'es2015' } = compilerOptions; const validBaseUrl = ['./', '', '.']; if (!validBaseUrl.includes(baseUrl)) { const formattedBaseUrl = validBaseUrl.map(x => `'${x}'`).join(', '); context.logger.warn(core_1.tags.oneLine `Root tsconfig option 'baseUrl' is not one of: ${formattedBaseUrl}. This might cause unexpected behaviour when generating libraries.`); } if (module !== 'es2015') { context.logger.warn(`Root tsconfig option 'module' is not 'es2015'. This might cause unexpected behaviour.`); } compilerOptions.module = module; compilerOptions.baseUrl = baseUrl; host.overwrite(tsConfigPath, JSON.stringify(tsCfgAst.value, null, 2)); return host; }; } function default_1() { return (host, context) => { if (host.exists('/.angular.json') || host.exists('/angular.json')) { context.logger.info('Found a modern configuration file. Nothing to be done.'); return host; } const configPath = getConfigPath(host); const configBuffer = host.read(core_1.normalize(configPath)); if (configBuffer == null) { throw new schematics_1.SchematicsException(`Could not find configuration file (${configPath})`); } const config = core_1.parseJson(configBuffer.toString(), core_1.JsonParseMode.Loose); if (typeof config != 'object' || Array.isArray(config) || config === null) { throw new schematics_1.SchematicsException('Invalid angular-cli.json configuration; expected an object.'); } return schematics_1.chain([ migrateKarmaConfiguration(config), migrateConfiguration(config, context.logger), updateSpecTsConfig(config), updatePackageJson(config), updateRootTsConfig(), updateTsLintConfig(), (host, context) => { context.logger.warn(core_1.tags.oneLine `Some configuration options have been changed, please make sure to update any npm scripts which you may have modified.`); return host; }, ]); }; } exports.default = default_1;