import { join, JsonObject, normalize, Path } from '@angular-devkit/core'; import { apply, chain, externalSchematic, filter, mergeWith, move, noop, Rule, SchematicContext, template, Tree, url } from '@angular-devkit/schematics'; import { addLintFiles, formatFiles, generateProjectLint, insert, names, NxJson, offsetFromRoot, toFileName, updateJsonInTree, updateWorkspace } from '@nrwl/workspace'; import { addDepsToPackageJson, updateWorkspaceInTree } from '@nrwl/workspace/src/utils/ast-utils'; import init from '../init/init'; import * as ts from 'typescript'; import { Schema } from './schema'; import { CSS_IN_JS_DEPENDENCIES } from '../../utils/styled'; import { addInitialRoutes } from '../../utils/ast-utils'; import { typesReactRouterDomVersion, reactRouterDomVersion } from '../../utils/versions'; import { assertValidStyle } from '../../utils/assertion'; import { extraEslintDependencies, reactEslintJson } from '../../utils/lint'; import { updateJestConfigContent } from '../../utils/jest-utils'; import { toJS } from '@nrwl/workspace/src/utils/rules/to-js'; interface NormalizedSchema extends Schema { projectName: string; appProjectRoot: Path; e2eProjectName: string; e2eProjectRoot: Path; parsedTags: string[]; fileName: string; styledModule: null | string; hasStyles: boolean; } export default function(schema: Schema): Rule { return (host: Tree, context: SchematicContext) => { const options = normalizeOptions(host, schema); return chain([ init({ skipFormat: true }), addLintFiles(options.appProjectRoot, options.linter, { localConfig: reactEslintJson, extraPackageDeps: extraEslintDependencies }), createApplicationFiles(options), updateNxJson(options), addProject(options), addCypress(options), addJest(options), updateJestConfig(options), addStyledModuleDependencies(options), addRouting(options, context), setDefaults(options), formatFiles(options) ]); }; } function createApplicationFiles(options: NormalizedSchema): Rule { return mergeWith( apply(url(`./files/app`), [ template({ ...names(options.name), ...options, tmpl: '', offsetFromRoot: offsetFromRoot(options.appProjectRoot) }), options.styledModule || !options.hasStyles ? filter(file => !file.endsWith(`.${options.style}`)) : noop(), options.unitTestRunner === 'none' ? filter(file => file !== `/src/app/${options.fileName}.spec.tsx`) : noop(), move(options.appProjectRoot), options.js ? toJS() : noop() ]) ); } function updateJestConfig(options: NormalizedSchema): Rule { return options.unitTestRunner === 'none' ? noop() : host => { const configPath = `${options.appProjectRoot}/jest.config.js`; const originalContent = host.read(configPath).toString(); const content = updateJestConfigContent(originalContent); host.overwrite(configPath, content); }; } function updateNxJson(options: NormalizedSchema): Rule { return updateJsonInTree('nx.json', json => { json.projects[options.projectName] = { tags: options.parsedTags }; return json; }); } function addProject(options: NormalizedSchema): Rule { return updateWorkspaceInTree(json => { const architect: { [key: string]: any } = {}; architect.build = { builder: '@nrwl/web:build', options: { outputPath: join(normalize('dist'), options.appProjectRoot), index: join(options.appProjectRoot, 'src/index.html'), main: join(options.appProjectRoot, maybeJs(options, `src/main.tsx`)), polyfills: join( options.appProjectRoot, maybeJs(options, 'src/polyfills.ts') ), tsConfig: join(options.appProjectRoot, 'tsconfig.app.json'), assets: [ join(options.appProjectRoot, 'src/favicon.ico'), join(options.appProjectRoot, 'src/assets') ], styles: options.styledModule || !options.hasStyles ? [] : [join(options.appProjectRoot, `src/styles.${options.style}`)], scripts: [], webpackConfig: '@nrwl/react/plugins/webpack' }, configurations: { production: { fileReplacements: [ { replace: join( options.appProjectRoot, maybeJs(options, `src/environments/environment.ts`) ), with: join( options.appProjectRoot, maybeJs(options, `src/environments/environment.prod.ts`) ) } ], optimization: true, outputHashing: 'all', sourceMap: false, extractCss: true, namedChunks: false, extractLicenses: true, vendorChunk: false, budgets: [ { type: 'initial', maximumWarning: '2mb', maximumError: '5mb' } ] } } }; architect.serve = { builder: '@nrwl/web:dev-server', options: { buildTarget: `${options.projectName}:build` }, configurations: { production: { buildTarget: `${options.projectName}:build:production` } } }; architect.lint = generateProjectLint( normalize(options.appProjectRoot), join(normalize(options.appProjectRoot), 'tsconfig.app.json'), options.linter ); json.projects[options.projectName] = { root: options.appProjectRoot, sourceRoot: join(options.appProjectRoot, 'src'), projectType: 'application', schematics: {}, architect }; json.defaultProject = json.defaultProject || options.projectName; return json; }); } function addCypress(options: NormalizedSchema): Rule { return options.e2eTestRunner === 'cypress' ? externalSchematic('@nrwl/cypress', 'cypress-project', { ...options, name: options.name + '-e2e', directory: options.directory, project: options.projectName }) : noop(); } function addJest(options: NormalizedSchema): Rule { return options.unitTestRunner === 'jest' ? externalSchematic('@nrwl/jest', 'jest-project', { project: options.projectName, supportTsx: true, skipSerializers: true, setupFile: 'none' }) : noop(); } function addStyledModuleDependencies(options: NormalizedSchema): Rule { const extraDependencies = CSS_IN_JS_DEPENDENCIES[options.styledModule]; return extraDependencies ? addDepsToPackageJson( extraDependencies.dependencies, extraDependencies.devDependencies ) : noop(); } function addRouting( options: NormalizedSchema, context: SchematicContext ): Rule { return options.routing ? chain([ function addRouterToComponent(host: Tree) { const appPath = join( options.appProjectRoot, maybeJs(options, `src/app/${options.fileName}.tsx`) ); const appFileContent = host.read(appPath).toString('utf-8'); const appSource = ts.createSourceFile( appPath, appFileContent, ts.ScriptTarget.Latest, true ); insert(host, appPath, addInitialRoutes(appPath, appSource, context)); }, addDepsToPackageJson( { 'react-router-dom': reactRouterDomVersion }, { '@types/react-router-dom': typesReactRouterDomVersion } ) ]) : noop(); } function setDefaults(options: NormalizedSchema): Rule { return options.skipWorkspaceJson ? noop() : updateWorkspace(workspace => { workspace.extensions.schematics = jsonIdentity( workspace.extensions.schematics || {} ); workspace.extensions.schematics['@nrwl/react'] = workspace.extensions.schematics['@nrwl/react'] || {}; const prev = jsonIdentity( workspace.extensions.schematics['@nrwl/react'] ); workspace.extensions.schematics = { ...workspace.extensions.schematics, '@nrwl/react': { ...prev, application: { style: options.style, linter: options.linter, ...jsonIdentity(prev.application) }, component: { style: options.style, ...jsonIdentity(prev.component) }, library: { style: options.style, linter: options.linter, ...jsonIdentity(prev.library) } } }; }); } function jsonIdentity(x: any): JsonObject { return x as JsonObject; } function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { const appDirectory = options.directory ? `${toFileName(options.directory)}/${toFileName(options.name)}` : toFileName(options.name); const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-'); const e2eProjectName = `${appProjectName}-e2e`; const appProjectRoot = normalize(`apps/${appDirectory}`); const e2eProjectRoot = normalize(`apps/${appDirectory}-e2e`); const parsedTags = options.tags ? options.tags.split(',').map(s => s.trim()) : []; const fileName = options.pascalCaseFiles ? 'App' : 'app'; const styledModule = /^(css|scss|less|styl|none)$/.test(options.style) ? null : options.style; assertValidStyle(options.style); return { ...options, name: toFileName(options.name), projectName: appProjectName, appProjectRoot, e2eProjectRoot, e2eProjectName, parsedTags, fileName, styledModule, hasStyles: options.style !== 'none' }; } function maybeJs(options: NormalizedSchema, path: string): string { return options.js && (path.endsWith('.ts') || path.endsWith('.tsx')) ? path.replace(/\.tsx?$/, '.js') : path; }