2020-03-03 14:44:10 -05:00

357 lines
9.8 KiB
TypeScript

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<NxJson>('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;
}