import { addDependenciesToPackageJson, addProjectConfiguration, convertNxGenerator, formatFiles, generateFiles, getWorkspacePath, installPackagesTask, joinPathFragments, names, normalizePath, offsetFromRoot, readJson, readProjectConfiguration, readWorkspaceConfiguration, Tree, updateJson, updateProjectConfiguration, updateWorkspaceConfiguration, visitNotIgnoredFiles, writeJson, } from '@nrwl/devkit'; import { readFileSync } from 'fs'; import { basename } from 'path'; import { serializeJson } from '../../utilities/fileutils'; import { resolveUserExistingPrettierConfig } from '../../utilities/prettier'; import { deduceDefaultBase } from '../../utilities/default-base'; import { angularCliVersion, nxVersion, prettierVersion, } from '../../utils/versions'; import { DEFAULT_NRWL_PRETTIER_CONFIG } from '../workspace/workspace'; import { Schema } from './schema'; function updatePackageJson(tree) { updateJson(tree, 'package.json', (packageJson) => { packageJson.scripts = packageJson.scripts || {}; packageJson.scripts = { ...packageJson.scripts, nx: 'nx', 'affected:apps': 'nx affected:apps', 'affected:libs': 'nx affected:libs', 'affected:build': 'nx affected:build', 'affected:e2e': 'nx affected:e2e', 'affected:test': 'nx affected:test', 'affected:lint': 'nx affected:lint', 'affected:dep-graph': 'nx affected:dep-graph', affected: 'nx affected', format: 'nx format:write', 'format:write': 'nx format:write', 'format:check': 'nx format:check', update: 'ng update @nrwl/workspace', 'update:check': 'ng update', lint: 'nx workspace-lint && ng lint', 'dep-graph': 'nx dep-graph', 'workspace-schematic': 'nx workspace-schematic', help: 'nx help', }; packageJson.devDependencies = packageJson.devDependencies || {}; if (!packageJson.dependencies) { packageJson.dependencies = {}; } if (!packageJson.dependencies['@nrwl/angular']) { packageJson.dependencies['@nrwl/angular'] = nxVersion; } if (!packageJson.devDependencies['@nrwl/workspace']) { packageJson.devDependencies['@nrwl/workspace'] = nxVersion; } if (!packageJson.devDependencies['@angular/cli']) { packageJson.devDependencies['@angular/cli'] = angularCliVersion; } if (!packageJson.devDependencies['prettier']) { packageJson.devDependencies['prettier'] = prettierVersion; } return packageJson; }); } function getRootTsConfigPath(host: Tree) { return host.exists('tsconfig.base.json') ? 'tsconfig.base.json' : 'tsconfig.json'; } function convertPath(name: string, originalPath: string) { return `apps/${name}/${originalPath}`; } function updateAngularCLIJson(host: Tree, options: Schema) { const workspaceConfig = readWorkspaceConfiguration(host); const appName = workspaceConfig.defaultProject; const e2eName = `${appName}-e2e`; const e2eRoot = joinPathFragments('apps', e2eName); delete (workspaceConfig as any).newProjectRoot; const defaultProject = readProjectConfiguration(host, appName); const oldSourceRoot = defaultProject.sourceRoot; const newRoot = joinPathFragments('apps', appName); defaultProject.root = newRoot; defaultProject.sourceRoot = joinPathFragments(newRoot, 'src'); function convertBuildOptions(buildOptions) { buildOptions.outputPath = buildOptions.outputPath && joinPathFragments(normalizePath('dist'), 'apps', appName); buildOptions.index = buildOptions.index && convertAsset(buildOptions.index as string); buildOptions.main = buildOptions.main && convertAsset(buildOptions.main as string); buildOptions.polyfills = buildOptions.polyfills && convertAsset(buildOptions.polyfills as string); buildOptions.tsConfig = buildOptions.tsConfig && joinPathFragments(newRoot, 'tsconfig.app.json'); buildOptions.assets = buildOptions.assets && (buildOptions.assets as any).map(convertAsset); buildOptions.styles = buildOptions.styles && (buildOptions.styles as any).map(convertAsset); buildOptions.scripts = buildOptions.scripts && (buildOptions.scripts as any).map(convertAsset); buildOptions.fileReplacements = buildOptions.fileReplacements && buildOptions.fileReplacements.map((replacement) => ({ replace: convertAsset(replacement.replace), with: convertAsset(replacement.with), })); } convertBuildOptions(defaultProject.targets.build.options); Object.values(defaultProject.targets.build.configurations).forEach((config) => convertBuildOptions(config) ); if (defaultProject.targets.test) { const testOptions = defaultProject.targets.test.options; testOptions.main = testOptions.main && convertAsset(testOptions.main); testOptions.polyfills = testOptions.polyfills && convertAsset(testOptions.polyfills); testOptions.tsConfig = joinPathFragments(newRoot, 'tsconfig.spec.json'); testOptions.karmaConfig = joinPathFragments(newRoot, 'karma.conf.js'); testOptions.assets = testOptions.assets && (testOptions.assets as any).map(convertAsset); testOptions.styles = testOptions.styles && (testOptions.styles as any).map(convertAsset); testOptions.scripts = testOptions.scripts && (testOptions.scripts as any).map(convertAsset); } const lintTarget = defaultProject.targets.lint; if (lintTarget) { lintTarget.options.tsConfig = [ joinPathFragments(newRoot, 'tsconfig.app.json'), joinPathFragments(newRoot, 'tsconfig.spec.json'), ]; } function convertServerOptions(serverOptions) { serverOptions.outputPath = serverOptions.outputPath && joinPathFragments( normalizePath('dist'), 'apps', `${options.name}-server` ); serverOptions.main = serverOptions.main && convertAsset(serverOptions.main); serverOptions.tsConfig = serverOptions.tsConfig && joinPathFragments(normalizePath('apps'), appName, 'tsconfig.server.json'); serverOptions.fileReplacements = serverOptions.fileReplacements && serverOptions.fileReplacements.map((replacement) => ({ replace: convertAsset(replacement.replace), with: convertAsset(replacement.with), })); } if (defaultProject.targets.server) { const serverOptions = defaultProject.targets.server.options; convertServerOptions(serverOptions); Object.values(defaultProject.targets.server.configurations).forEach( (config) => convertServerOptions(config) ); } if (defaultProject.targets.e2e) { const lintTargetOptions = lintTarget ? lintTarget.options : {}; addProjectConfiguration(host, e2eName, { root: e2eRoot, projectType: 'application', targets: { e2e: { ...defaultProject.targets.e2e, options: { ...defaultProject.targets.e2e.options, protractorConfig: joinPathFragments(e2eRoot, 'protractor.conf.js'), }, }, lint: { executor: '@angular-devkit/build-angular:tslint', options: { ...lintTargetOptions, tsConfig: joinPathFragments(e2eRoot, 'tsconfig.json'), }, }, }, implicitDependencies: [appName], tags: [], }); } updateProjectConfiguration(host, appName, defaultProject); workspaceConfig.cli = workspaceConfig.cli || ({} as any); if (!workspaceConfig.cli.defaultCollection) { workspaceConfig.cli.defaultCollection = '@nrwl/angular'; } updateWorkspaceConfiguration(host, workspaceConfig); function convertAsset(asset: string | any) { if (typeof asset === 'string') { return asset.startsWith(oldSourceRoot) ? convertPath(appName, asset.replace(oldSourceRoot, 'src')) : asset; } else { return { ...asset, input: asset.input && asset.input.startsWith(oldSourceRoot) ? convertPath(appName, asset.input.replace(oldSourceRoot, 'src')) : asset.input, }; } } } function updateTsConfig(host: Tree) { updateJson(host, 'nx.json', (json) => { json.implicitDependencies['tsconfig.base.json'] = '*'; return json; }); writeJson( host, 'tsconfig.base.json', setUpCompilerOptions(readJson(host, getRootTsConfigPath(host))) ); if (host.exists('tsconfig.json')) { host.delete('tsconfig.json'); } } function updateTsConfigsJson(host: Tree, options: Schema) { const workspaceJson = readJson(host, 'angular.json'); const app = workspaceJson.projects[options.name]; const e2eProject = getE2eProject(workspaceJson); const tsConfigPath = getRootTsConfigPath(host); const appOffset = offsetFromRoot(app.root); updateJson(host, app.targets.build.options.tsConfig, (json) => { json.extends = `${appOffset}${tsConfigPath}`; json.compilerOptions = json.compilerOptions || {}; json.compilerOptions.outDir = `${appOffset}dist/out-tsc`; return json; }); if (app.targets.test) { updateJson(host, app.targets.test.options.tsConfig, (json) => { json.extends = `${appOffset}${tsConfigPath}`; json.compilerOptions = json.compilerOptions || {}; json.compilerOptions.outDir = `${appOffset}dist/out-tsc`; return json; }); } if (app.targets.server) { updateJson(host, app.targets.server.options.tsConfig, (json) => { json.extends = `${appOffset}${tsConfigPath}`; json.compilerOptions = json.compilerOptions || {}; json.compilerOptions.outDir = `${appOffset}dist/out-tsc`; return json; }); } if (!!e2eProject) { updateJson(host, e2eProject.targets.lint.options.tsConfig, (json) => { json.extends = `${offsetFromRoot(e2eProject.root)}${tsConfigPath}`; json.compilerOptions = { ...json.compilerOptions, outDir: `${offsetFromRoot(e2eProject.root)}dist/out-tsc`, }; return json; }); } } function updateTsLint(host: Tree) { if (host.exists(`tslint.json`)) { updateJson(host, 'tslint.json', (tslintJson) => { [ 'no-trailing-whitespace', 'one-line', 'quotemark', 'typedef-whitespace', 'whitespace', ].forEach((key) => { tslintJson[key] = undefined; }); tslintJson.rulesDirectory = tslintJson.rulesDirectory || []; tslintJson.rulesDirectory.push('node_modules/@nrwl/workspace/src/tslint'); tslintJson.rules['nx-enforce-module-boundaries'] = [ true, { allow: [], depConstraints: [{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }], }, ]; return tslintJson; }); } } function updateProjectTsLint(host: Tree, options: Schema) { const workspaceJson = readJson(host, getWorkspacePath(host)); const app = workspaceJson.projects[options.name]; const offset = '../../'; if (host.exists(`${app.root}/tslint.json`)) { updateJson(host, `${app.root}/tslint.json`, (json) => { json.extends = `${offset}tslint.json`; return json; }); } } function setUpCompilerOptions(tsconfig: any): any { if (!tsconfig.compilerOptions.paths) { tsconfig.compilerOptions.paths = {}; } tsconfig.compilerOptions.baseUrl = '.'; tsconfig.compilerOptions.rootDir = '.'; return tsconfig; } function moveOutOfSrc( tree: Tree, appName: string, filePath: string, required = true ) { if (!filePath) { return; } const filename = !!filePath ? basename(filePath) : ''; const from = filePath; const to = filename ? joinPathFragments('apps', appName, filename) : joinPathFragments('apps', appName); renameSyncInTree(tree, from, to, required); } function getE2eKey(workspaceJson: any) { return Object.keys(workspaceJson.projects).find((key) => { return !!workspaceJson.projects[key]?.architect?.e2e; }); } function getE2eProject(workspaceJson: any) { const key = getE2eKey(workspaceJson); if (key) { return workspaceJson.projects[key]; } else { return null; } } function moveExistingFiles(host: Tree, options: Schema) { const workspaceJson = readJson(host, getWorkspacePath(host)); const app = workspaceJson.projects[options.name]; const e2eApp = getE2eProject(workspaceJson); // it is not required to have a browserslist moveOutOfSrc(host, options.name, 'browserslist', false); moveOutOfSrc(host, options.name, '.browserslistrc', false); moveOutOfSrc(host, options.name, app.architect.build.options.tsConfig); if (app.architect.test) { moveOutOfSrc(host, options.name, app.architect.test.options.karmaConfig); moveOutOfSrc(host, options.name, app.architect.test.options.tsConfig); } else { // there could still be a karma.conf.js file in the root // so move to new location if (host.exists('karma.conf.js')) { console.info('No test configuration, but root Karma config file found'); moveOutOfSrc(host, options.name, 'karma.conf.js'); } } if (app.architect.server) { moveOutOfSrc(host, options.name, app.architect.server.options.tsConfig); } const oldAppSourceRoot = app.sourceRoot; const newAppSourceRoot = joinPathFragments('apps', options.name, 'src'); renameDirSyncInTree(host, oldAppSourceRoot, newAppSourceRoot); if (e2eApp) { const oldE2eRoot = joinPathFragments(app.root || '', 'e2e'); const newE2eRoot = joinPathFragments( 'apps', `${getE2eKey(workspaceJson)}-e2e` ); renameDirSyncInTree(host, oldE2eRoot, newE2eRoot); } else { console.warn( 'No e2e project was migrated because there was none declared in angular.json' ); } return host; } async function createAdditionalFiles(host: Tree, options: Schema) { const workspaceJson = readJson(host, 'angular.json'); host.write( 'nx.json', serializeJson({ npmScope: options.npmScope, affected: { defaultBase: `${options.defaultBase}` || deduceDefaultBase(), }, implicitDependencies: { 'angular.json': '*', 'package.json': '*', 'tslint.json': '*', '.eslintrc.json': '*', 'nx.json': '*', }, projects: { [options.name]: { tags: [], }, [`${getE2eKey(workspaceJson)}-e2e`]: { tags: [], }, }, }) ); host.write('libs/.gitkeep', ''); const recommendations = [ 'nrwl.angular-console', 'angular.ng-template', 'dbaeumer.vscode-eslint', 'esbenp.prettier-vscode', ]; if (host.exists('.vscode/extensions.json')) { updateJson( host, '.vscode/extensions.json', (json: { recommendations?: string[] }) => { json.recommendations = json.recommendations || []; recommendations.forEach((extension) => { if (!json.recommendations.includes(extension)) { json.recommendations.push(extension); } }); return json; } ); } else { writeJson(host, '.vscode/extensions.json', { recommendations, }); } // if the user does not already have a prettier configuration // of any kind, create one const existingPrettierConfig = await resolveUserExistingPrettierConfig(); if (!existingPrettierConfig) { host.write('.prettierrc', serializeJson(DEFAULT_NRWL_PRETTIER_CONFIG)); } } function checkCanConvertToWorkspace(host: Tree) { try { if (!host.exists('package.json')) { throw new Error('Cannot find package.json'); } if (!host.exists('angular.json')) { throw new Error('Cannot find angular.json'); } // TODO: This restriction should be lited const workspaceJson = readJson(host, 'angular.json'); const hasLibraries = Object.keys(workspaceJson.projects).find( (project) => workspaceJson.projects[project].projectType && workspaceJson.projects[project].projectType !== 'application' ); if (Object.keys(workspaceJson.projects).length > 2 || hasLibraries) { throw new Error('Can only convert projects with one app'); } const e2eKey = getE2eKey(workspaceJson); const e2eApp = getE2eProject(workspaceJson); if (e2eApp && !host.exists(e2eApp.architect.e2e.options.protractorConfig)) { console.info( `Make sure the ${e2eKey}.architect.e2e.options.protractorConfig is valid or the ${e2eKey} project is removed from angular.json.` ); throw new Error( `An e2e project was specified but ${e2eApp.architect.e2e.options.protractorConfig} could not be found.` ); } return host; } catch (e) { console.error(e.message); console.error( 'Your workspace could not be converted into an Nx Workspace because of the above error.' ); throw e; } } function createNxJson(host: Tree) { const json = JSON.parse(host.read('angular.json').toString()); const projects = json.projects || {}; const hasLibraries = Object.keys(projects).find( (project) => projects[project].projectType && projects[project].projectType !== 'application' ); if (Object.keys(projects).length !== 1 || hasLibraries) { throw new Error( `The schematic can only be used with Angular CLI workspaces with a single application.` ); } const name = Object.keys(projects)[0]; const tsConfigPath = getRootTsConfigPath(host); host.write( 'nx.json', serializeJson({ npmScope: name, implicitDependencies: { 'angular.json': '*', 'package.json': '*', [tsConfigPath]: '*', 'tslint.json': '*', '.eslintrc.json': '*', 'nx.json': '*', }, projects: { [name]: { tags: [], }, }, tasksRunnerOptions: { default: { runner: '@nrwl/workspace/tasks-runners/default', options: { cacheableOperations: ['build', 'lint', 'test', 'e2e'], }, }, }, }) ); } function decorateAngularClI(host: Tree) { const decorateCli = readFileSync( joinPathFragments( __dirname as any, '..', 'utils', 'decorate-angular-cli.js__tmpl__' ) ).toString(); host.write('decorate-angular-cli.js', decorateCli); updateJson(host, 'package.json', (json) => { if ( json.scripts && json.scripts.postinstall && !json.scripts.postinstall.includes('decorate-angular-cli.js') ) { // if exists, add execution of this script json.scripts.postinstall += ' && node ./decorate-angular-cli.js'; } else { if (!json.scripts) json.scripts = {}; // if doesn't exist, set to execute this script json.scripts.postinstall = 'node ./decorate-angular-cli.js'; } if (json.scripts.ng) { json.scripts.ng = 'nx'; } return json; }); } function addFiles(host: Tree) { generateFiles(host, joinPathFragments(__dirname, './files/root'), '.', { tmpl: '', }); if (!host.exists('.prettierignore')) { generateFiles(host, joinPathFragments(__dirname, './files/prettier'), '.', { tmpl: '', }); } } function renameSyncInTree( tree: Tree, from: string, to: string, required: boolean ) { if (!tree.exists(from)) { if (required) { console.warn(`Path: ${from} does not exist`); } } else if (tree.exists(to)) { if (required) { console.warn(`Path: ${to} already exists`); } } else { const contents = tree.read(from); tree.write(to, contents); tree.delete(from); } } function renameDirSyncInTree(tree: Tree, from: string, to: string) { visitNotIgnoredFiles(tree, from, (file) => { renameSyncInTree(tree, file, file.replace(from, to), true); }); } export async function initGenerator(tree: Tree, schema: Schema) { if (schema.preserveAngularCLILayout) { addDependenciesToPackageJson(tree, {}, { '@nrwl/workspace': nxVersion }); createNxJson(tree); decorateAngularClI(tree); } else { const options = { ...schema, npmScope: names(schema.npmScope || schema.name).fileName, }; checkCanConvertToWorkspace(tree); moveExistingFiles(tree, options); addFiles(tree); await createAdditionalFiles(tree, options); updatePackageJson(tree); updateAngularCLIJson(tree, options); updateTsLint(tree); updateProjectTsLint(tree, options); updateTsConfig(tree); updateTsConfigsJson(tree, options); decorateAngularClI(tree); await formatFiles(tree); } if (!schema.skipInstall) { return () => { installPackagesTask(tree); }; } } export const initSchematic = convertNxGenerator(initGenerator); export default initGenerator;