import { Rule, chain, SchematicContext, Tree, TaskConfigurationGenerator, TaskConfiguration, TaskExecutorFactory, externalSchematic } from '@angular-devkit/schematics'; import { stripIndents } from '@angular-devkit/core/src/utils/literals'; import { readJsonInTree, addDepsToPackageJson, updateJsonInTree, insert, formatFiles } from '@nrwl/workspace'; import { createSourceFile, ScriptTarget, isImportDeclaration, isStringLiteral } from 'typescript'; import { getSourceNodes, ReplaceChange } from '@nrwl/workspace/src/utils/ast-utils'; import { relative } from 'path'; import { RunSchematicTaskOptions } from '@angular-devkit/schematics/tasks/run-schematic/options'; import * as path from 'path'; import { platform } from 'os'; import { Observable } from 'rxjs'; import { spawn } from 'child_process'; const ignore = require('ignore'); export class RunUpdateTask implements TaskConfigurationGenerator { protected _package: string; constructor(pkg: string) { this._package = pkg; } toConfiguration(): TaskConfiguration { return { name: 'RunUpdate', options: { package: this._package } }; } } function createRunUpdateTask(): TaskExecutorFactory { return { name: 'RunUpdate', create: () => { return Promise.resolve((options: any, context: SchematicContext) => { context.logger.info(`Updating ${options.package}`); const spawnOptions = { stdio: [process.stdin, process.stdout, process.stderr], shell: true }; const ng = platform() === 'win32' ? '.\\node_modules\\.bin\\ng' : './node_modules/.bin/ng'; const args = [ 'update', options.package, '--force', '--allow-dirty' ].filter(e => !!e); return new Observable(obs => { spawn(ng, args, spawnOptions).on('close', (code: number) => { if (code === 0) { obs.next(); obs.complete(); } else { const message = `${options.package} migration failed, see above.`; obs.error(new Error(message)); } }); }); }); } }; } function addDependencies() { return (host: Tree, context: SchematicContext) => { const dependencies = readJsonInTree(host, 'package.json').dependencies; const builders = new Set(); const projects = readJsonInTree(host, 'angular.json').projects; Object.values(projects).forEach(project => { Object.values(project.architect).forEach(target => { const [builderDependency] = target.builder.split(':'); builders.add(builderDependency); }); }); const newDependencies = {}; const newDevDependencies = { '@nrwl/workspace': '8.0.0' }; context.logger.info(`Adding @nrwl/workspace as a dependency`); if (dependencies['@angular/core']) { newDependencies['@nrwl/angular'] = '8.0.0'; context.logger.info(`Adding @nrwl/angular as a dependency`); } if (dependencies['react']) { newDevDependencies['@nrwl/react'] = '8.0.0'; context.logger.info(`Adding @nrwl/react as a dependency`); } if (dependencies['@nestjs/core']) { newDevDependencies['@nrwl/nest'] = '8.0.0'; context.logger.info(`Adding @nrwl/nest as a dependency`); } if (dependencies.express) { newDevDependencies['@nrwl/express'] = '8.0.0'; newDevDependencies['@nrwl/node'] = '8.0.0'; context.logger.info(`Adding @nrwl/express as a dependency`); } if (builders.has('@nrwl/web')) { newDevDependencies['@nrwl/web'] = '8.0.0'; context.logger.info(`Adding @nrwl/web as a dependency`); } if (builders.has('@nrwl/node')) { newDevDependencies['@nrwl/node'] = '8.0.0'; context.logger.info(`Adding @nrwl/node as a dependency`); } if (builders.has('@nrwl/jest')) { newDevDependencies['@nrwl/jest'] = '8.0.0'; context.logger.info(`Adding @nrwl/jest as a dependency`); } if (builders.has('@nrwl/cypress')) { newDevDependencies['@nrwl/cypress'] = '8.0.0'; context.logger.info(`Adding @nrwl/cypress as a dependency`); } return chain([addDepsToPackageJson(newDependencies, newDevDependencies)]); }; } const removeOldDependencies = updateJsonInTree( 'package.json', (json, context: SchematicContext) => { json.dependencies = json.dependencies || {}; json.devDependencies = json.devDependencies || {}; delete json.dependencies['@nrwl/nx']; delete json.devDependencies['@nrwl/nx']; delete json.dependencies['@nrwl/schematics']; delete json.devDependencies['@nrwl/schematics']; delete json.dependencies['@nrwl/builders']; delete json.devDependencies['@nrwl/builders']; context.logger.info(`Removing @nrwl/schematics as a dependency`); context.logger.info(`Removing @nrwl/builders as a dependency`); context.logger.info(`Removing @nrwl/nx as a dependency`); return json; } ); const updateUpdateScript = updateJsonInTree('package.json', json => { json.scripts = json.scripts || {}; json.scripts.update = 'ng update @nrwl/workspace'; return json; }); const updateBuilders = updateJsonInTree('angular.json', json => { Object.entries(json.projects).forEach(([projectKey, project]) => { Object.entries(project.architect).forEach(([targetKey, target]) => { if (target.builder === '@nrwl/builders:jest') { json.projects[projectKey].architect[targetKey].builder = '@nrwl/jest:jest'; } if (target.builder === '@nrwl/builders:cypress') { json.projects[projectKey].architect[targetKey].builder = '@nrwl/cypress:cypress'; } if (target.builder === '@nrwl/builders:web-build') { json.projects[projectKey].architect[targetKey].builder = '@nrwl/web:build'; } if (target.builder === '@nrwl/builders:web-dev-server') { json.projects[projectKey].architect[targetKey].builder = '@nrwl/web:dev-server'; } if (target.builder === '@nrwl/builders:node-build') { json.projects[projectKey].architect[targetKey].builder = '@nrwl/node:build'; } if (target.builder === '@nrwl/builders:node-execute') { json.projects[projectKey].architect[targetKey].builder = '@nrwl/node:execute'; } if (target.builder === '@nrwl/builders:run-commands') { json.projects[projectKey].architect[targetKey].builder = '@nrwl/workspace:run-commands'; } }); }); return json; }); const displayInformation = (host: Tree, context: SchematicContext) => { context.logger.info(stripIndents` Nx has been repackaged. We are installing and migrating your dependencies to the ones necessary. If you have workspace schematics, we tried to migrate your imports from "@nrwl/schematics" to "@nrwl/workspace" but your externalSchematics may be broken. Read this guide to see where to find familiar features: https://nx.dev/guides/nx7-to-nx8 This migration may take a few minutes. `); }; const updateNxModuleImports = (host: Tree) => { let ig; if (host.exists('.gitignore')) { ig = ignore(); ig.add(host.read('.gitignore').toString()); } host.visit(path => { if (!path.endsWith('.ts')) { return; } if (ig && ig.ignores(relative('/', path))) { return; } const sourceFile = createSourceFile( path, host.read(path).toString(), ScriptTarget.Latest, true ); const changes = []; sourceFile.statements.forEach(statement => { if ( isImportDeclaration(statement) && isStringLiteral(statement.moduleSpecifier) ) { const nodeText = statement.moduleSpecifier.getText(sourceFile); const modulePath = statement.moduleSpecifier .getText(sourceFile) .substr(1, nodeText.length - 2); if (modulePath === '@nrwl/nx') { changes.push( new ReplaceChange( path, statement.moduleSpecifier.getStart(sourceFile), nodeText, `'@nrwl/angular'` ) ); } if (modulePath === '@nrwl/nx/testing') { changes.push( new ReplaceChange( path, statement.moduleSpecifier.getStart(sourceFile), nodeText, `'@nrwl/angular/testing'` ) ); } if (modulePath.startsWith('@nrwl/schematics')) { changes.push( new ReplaceChange( path, statement.moduleSpecifier.getStart(sourceFile), nodeText, nodeText.replace('@nrwl/schematics', '@nrwl/workspace') ) ); } } }); insert(host, path, changes); }); }; const updateJestPlugin = (host: Tree) => { if (!host.exists('jest.config.js')) { return host; } const sourceFile = createSourceFile( 'jest.config.js', host.read('jest.config.js').toString(), ScriptTarget.Latest, true ); const changes = []; getSourceNodes(sourceFile).forEach(node => { if (isStringLiteral(node)) { const value = node .getText(sourceFile) .substr(1, node.getText(sourceFile).length - 2); if (value === '@nrwl/builders/plugins/jest/resolver') { changes.push( new ReplaceChange( 'jest.config.js', node.getStart(sourceFile), node.getText(sourceFile), `'@nrwl/jest/plugins/resolver'` ) ); } } }); insert(host, 'jest.config.js', changes); }; const updateTslintRules = updateJsonInTree('tslint.json', json => { const { rulesDirectory } = json; json.rulesDirectory = rulesDirectory.map(directory => { return directory === 'node_modules/@nrwl/schematics/src/tslint' ? 'node_modules/@nrwl/workspace/src/tslint' : directory; }); return json; }); const updateDefaultCollection = (host: Tree, context: SchematicContext) => { const { dependencies, devDependencies } = readJsonInTree( host, 'package.json' ); return updateJsonInTree('angular.json', json => { json.cli = json.cli || {}; if (dependencies['@nrwl/angular']) { json.cli.defaultCollection = '@nrwl/angular'; } else if (devDependencies['@nrwl/react']) { json.cli.defaultCollection = '@nrwl/react'; } else if (devDependencies['@nrwl/nest']) { json.cli.defaultCollection = '@nrwl/nest'; } else if (devDependencies['@nrwl/express']) { json.cli.defaultCollection = '@nrwl/express'; } else if (devDependencies['@nrwl/web']) { json.cli.defaultCollection = '@nrwl/web'; } else if (devDependencies['@nrwl/node']) { json.cli.defaultCollection = '@nrwl/node'; } else { json.cli.defaultCollection = '@nrwl/workspace'; } context.logger.info( `Default collection is now set to ${json.cli.defaultCollection}` ); return json; }); }; const setRootDirAndUpdateOurDir = (host: Tree) => { let ig; if (host.exists('.gitignore')) { ig = ignore(); ig.add(host.read('.gitignore').toString()); } host.visit(path => { if (!path.endsWith('.json')) { return; } if (ig && ig.ignores(relative('/', path))) { return; } const json = host.read(path).toString(); const match = json.match(/"outDir"\s*:\s*"([^"]+)"/); if (match) { const outParts = match[1].split('out-tsc'); if (outParts.length > 1) { const updatedJson = json.replace( /"outDir"\s*:\s*"([^"]+)"/, `"outDir": "${outParts[0]}out-tsc"` ); host.overwrite(path, updatedJson); } } }); updateJsonInTree('tsconfig.json', json => { json.compilerOptions = json.compilerOptions || {}; json.compilerOptions.rootDir = '.'; return json; })(host, null); }; export const runAngularMigrations: Rule = ( host: Tree, context: SchematicContext ) => { // Should always be there during ng update but not during tests. if (!context.engine.workflow) { return; } const packageJson = readJsonInTree(host, 'package.json'); const engineHost = (context.engine.workflow as any)._engineHost; engineHost.registerTaskExecutor(createRunUpdateTask()); const cliUpgrade = context.addTask(new RunUpdateTask('@angular/cli')); if (packageJson.dependencies['@angular/core']) { context.addTask(new RunUpdateTask('@angular/core'), [cliUpgrade]); } }; const updateNestDependencies = updateJsonInTree('package.json', json => { json.dependencies = json.dependencies || {}; json.devDependencies = json.devDependencies || {}; if (!json.devDependencies['@nrwl/nest']) { return json; } const nestFrameworkVersion = '^6.2.4'; json.dependencies = { ...json.dependencies, '@nestjs/common': nestFrameworkVersion, '@nestjs/core': nestFrameworkVersion, '@nestjs/platform-express': nestFrameworkVersion, 'reflect-metadata': '^0.1.12' }; json.devDependencies = { ...json.devDependencies, '@nestjs/schematics': '^6.3.0', '@nestjs/testing': nestFrameworkVersion }; return json; }); export default function(): Rule { return chain([ displayInformation, runAngularMigrations, removeOldDependencies, updateUpdateScript, updateBuilders, updateJestPlugin, updateNxModuleImports, updateTslintRules, addDependencies(), updateNestDependencies, updateDefaultCollection, setRootDirAndUpdateOurDir, formatFiles() ]); }