feat(core): migrate create-nx-workspace to devkit

This commit is contained in:
Jason Jean 2021-01-07 20:40:28 -05:00 committed by Victor Savkin
parent 764f580e99
commit 09c78c4750
18 changed files with 955 additions and 349 deletions

View File

@ -11,6 +11,7 @@ export {
NxJsonProjectConfiguration,
} from '@nrwl/tao/src/shared/nx';
export { logger } from '@nrwl/tao/src/shared/logger';
export { getPackageManagerCommand } from '@nrwl/tao/src/shared/package-manager';
export { TargetContext } from '@nrwl/tao/src/commands/run';
export { formatFiles } from './src/generators/format-files';
@ -25,7 +26,10 @@ export { readJson, writeJson, updateJson } from './src/utils/json';
export { addDependenciesToPackageJson } from './src/utils/package-json';
export { installPackagesTask } from './src/tasks/install-packages-task';
export { names } from './src/utils/names';
export { getWorkspaceLayout } from './src/utils/get-workspace-layout';
export {
getWorkspaceLayout,
getWorkspacePath,
} from './src/utils/get-workspace-layout';
export {
applyChangesToString,
ChangeType,

View File

@ -1,6 +1,7 @@
import { Tree } from '@nrwl/tao/src/shared/tree';
import { execSync } from 'child_process';
import { getPackageManagerCommand } from '@nrwl/tao/src/shared/package-manager';
import { join } from 'path';
let storedPackageJsonValue;
@ -11,14 +12,21 @@ let storedPackageJsonValue;
* @param host - the file system tree
* @param alwaysRun - always run the command even if `package.json` hasn't changed.
*/
export function installPackagesTask(host: Tree, alwaysRun: boolean = false) {
const packageJsonValue = host.read('package.json').toString();
if (host.listChanges().find((f) => f.path === 'package.json') || alwaysRun) {
export function installPackagesTask(
host: Tree,
alwaysRun: boolean = false,
cwd: string = ''
) {
const packageJsonValue = host.read(join(cwd, 'package.json')).toString();
if (
host.listChanges().find((f) => f.path === join(cwd, 'package.json')) ||
alwaysRun
) {
if (storedPackageJsonValue != packageJsonValue || alwaysRun) {
storedPackageJsonValue = host.read('package.json').toString();
storedPackageJsonValue = host.read(join(cwd, 'package.json')).toString();
const pmc = getPackageManagerCommand();
execSync(pmc.install, {
cwd: host.root,
cwd: join(host.root, cwd),
stdio: [0, 1, 2],
});
}

View File

@ -179,14 +179,31 @@ export async function taoNew(cwd: string, args: string[], isVerbose = false) {
null,
null
);
return (await import('./ngcli-adapter')).invokeNew(
cwd,
{
...opts,
generatorOptions: combinedOpts,
},
isVerbose
);
if (ws.isNxGenerator(opts.collectionName, normalizedGeneratorName)) {
const host = new FsTree(cwd, isVerbose);
const task = await implementation(host, combinedOpts);
const changes = host.listChanges();
printChanges(changes);
if (!opts.dryRun) {
flushChanges(cwd, changes);
if (task) {
await task();
}
} else {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}
} else {
return (await import('./ngcli-adapter')).generate(
cwd,
{
...opts,
generatorOptions: combinedOpts,
},
isVerbose
);
}
});
}

View File

@ -95,6 +95,42 @@ describe('tree', () => {
).toEqual('new child content');
});
it('should normalize paths', () => {
tree.write('dir/file1', 'File 1 Contents');
tree.write('/dir/file2', 'File 2 Contents');
tree.write('./dir/file3', 'File 3 Contents');
expect(tree.read('dir/file1').toString()).toEqual('File 1 Contents');
expect(tree.read('/dir/file1').toString()).toEqual('File 1 Contents');
expect(tree.read('./dir/file1').toString()).toEqual('File 1 Contents');
expect(tree.read('dir/file2').toString()).toEqual('File 2 Contents');
expect(tree.read('/dir/file2').toString()).toEqual('File 2 Contents');
expect(tree.read('./dir/file2').toString()).toEqual('File 2 Contents');
expect(tree.read('dir/file3').toString()).toEqual('File 3 Contents');
expect(tree.read('/dir/file3').toString()).toEqual('File 3 Contents');
expect(tree.read('./dir/file3').toString()).toEqual('File 3 Contents');
tree.rename('dir/file1', 'dir/file-a');
expect(tree.read('dir/file-a').toString()).toEqual('File 1 Contents');
expect(tree.read('/dir/file-a').toString()).toEqual('File 1 Contents');
expect(tree.read('./dir/file-a').toString()).toEqual('File 1 Contents');
tree.rename('/dir/file2', '/dir/file-b');
expect(tree.read('dir/file-b').toString()).toEqual('File 2 Contents');
expect(tree.read('/dir/file-b').toString()).toEqual('File 2 Contents');
expect(tree.read('./dir/file-b').toString()).toEqual('File 2 Contents');
tree.rename('./dir/file3', './dir/file-c');
expect(tree.read('dir/file-c').toString()).toEqual('File 3 Contents');
expect(tree.read('/dir/file-c').toString()).toEqual('File 3 Contents');
expect(tree.read('./dir/file-c').toString()).toEqual('File 3 Contents');
});
it('should be able to delete files', () => {
tree.delete('parent/parent-file.txt');
tree.write('parent/new-child/new-child-file.txt', 'new child content');

View File

@ -1,4 +1,3 @@
import * as path from 'path';
import {
readdirSync,
readFileSync,
@ -8,6 +7,7 @@ import {
} from 'fs';
import { mkdirpSync, rmdirSync } from 'fs-extra';
import { logger } from './logger';
import { dirname, join, relative } from 'path';
const chalk = require('chalk');
/**
@ -88,6 +88,7 @@ export class FsTree implements Tree {
constructor(readonly root: string, private readonly isVerbose: boolean) {}
read(filePath: string): Buffer | null {
filePath = this.normalize(filePath);
try {
if (this.recordedChanges[this.rp(filePath)]) {
return this.recordedChanges[this.rp(filePath)].content;
@ -103,6 +104,7 @@ export class FsTree implements Tree {
}
write(filePath: string, content: Buffer | string): void {
filePath = this.normalize(filePath);
try {
this.recordedChanges[this.rp(filePath)] = {
content: Buffer.from(content),
@ -116,10 +118,12 @@ export class FsTree implements Tree {
}
overwrite(filePath: string, content: Buffer | string): void {
filePath = this.normalize(filePath);
this.write(filePath, content);
}
exists(filePath: string): boolean {
filePath = this.normalize(filePath);
try {
if (this.recordedChanges[this.rp(filePath)]) {
return !this.recordedChanges[this.rp(filePath)].isDeleted;
@ -134,6 +138,7 @@ export class FsTree implements Tree {
}
delete(filePath: string): void {
filePath = this.normalize(filePath);
if (this.filesForDir(this.rp(filePath)).length > 0) {
this.filesForDir(this.rp(filePath)).forEach(
(f) => (this.recordedChanges[f] = { content: null, isDeleted: true })
@ -146,12 +151,15 @@ export class FsTree implements Tree {
}
rename(from: string, to: string): void {
from = this.normalize(from);
to = this.normalize(to);
const content = this.read(this.rp(from));
this.recordedChanges[this.rp(from)] = { content: null, isDeleted: true };
this.recordedChanges[this.rp(to)] = { content: content, isDeleted: false };
}
isFile(filePath: string): boolean {
filePath = this.normalize(filePath);
try {
if (this.recordedChanges[this.rp(filePath)]) {
return !this.recordedChanges[this.rp(filePath)].isDeleted;
@ -164,11 +172,12 @@ export class FsTree implements Tree {
}
children(dirPath: string): string[] {
dirPath = this.normalize(dirPath);
let res = this.fsReadDir(dirPath);
res = [...res, ...this.directChildrenOfDir(this.rp(dirPath))];
return res.filter((q) => {
const r = this.recordedChanges[path.join(this.rp(dirPath), q)];
const r = this.recordedChanges[join(this.rp(dirPath), q)];
if (r && r.isDeleted) return false;
return true;
});
@ -200,10 +209,14 @@ export class FsTree implements Tree {
return res;
}
private normalize(path: string) {
return relative(this.root, join(this.root, path));
}
private fsReadDir(dirPath: string) {
if (!this.delegateToFs) return [];
try {
return readdirSync(path.join(this.root, dirPath));
return readdirSync(join(this.root, dirPath));
} catch (e) {
return [];
}
@ -211,19 +224,19 @@ export class FsTree implements Tree {
private fsIsFile(filePath: string) {
if (!this.delegateToFs) return false;
const stat = statSync(path.join(this.root, filePath));
const stat = statSync(join(this.root, filePath));
return stat.isFile();
}
private fsReadFile(filePath: string) {
if (!this.delegateToFs) return null;
return readFileSync(path.join(this.root, filePath));
return readFileSync(join(this.root, filePath));
}
private fsExists(filePath: string): boolean {
if (!this.delegateToFs) return false;
try {
const stat = statSync(path.join(this.root, filePath));
const stat = statSync(join(this.root, filePath));
return stat.isFile() || stat.isDirectory();
} catch (e) {
return false;
@ -258,9 +271,9 @@ export class FsTree implements Tree {
export function flushChanges(root: string, fileChanges: FileChange[]) {
fileChanges.forEach((f) => {
const fpath = path.join(root, f.path);
const fpath = join(root, f.path);
if (f.type === 'CREATE') {
mkdirpSync(path.dirname(fpath));
mkdirpSync(dirname(fpath));
writeFileSync(fpath, f.content);
} else if (f.type === 'UPDATE') {
writeFileSync(fpath, f.content);

View File

@ -3,7 +3,7 @@
"version": "0.1",
"schematics": {
"workspace": {
"factory": "./src/schematics/workspace/workspace",
"factory": "./src/schematics/workspace/workspace#workspaceSchematic",
"schema": "./src/schematics/workspace/schema.json",
"description": "Create an empty workspace",
"hidden": true
@ -38,7 +38,71 @@
},
"new": {
"factory": "./src/schematics/new/new",
"factory": "./src/schematics/new/new#newSchematic",
"schema": "./src/schematics/new/schema.json",
"description": "Create a workspace",
"hidden": true
},
"library": {
"factory": "./src/schematics/library/library",
"schema": "./src/schematics/library/schema.json",
"aliases": ["lib"],
"description": "Create a library"
},
"workspace-generator": {
"factory": "./src/schematics/workspace-generator/workspace-generator",
"schema": "./src/schematics/workspace-generator/schema.json",
"aliases": ["workspace-schematic"],
"description": "Generates a workspace generator"
},
"run-commands": {
"factory": "./src/schematics/run-commands/run-commands",
"schema": "./src/schematics/run-commands/schema.json",
"aliases": ["run-command", "target"],
"description": "Generates a target to run any command in the terminal"
}
},
"generators": {
"workspace": {
"factory": "./src/schematics/workspace/workspace#workspaceGenerator",
"schema": "./src/schematics/workspace/schema.json",
"description": "Create an empty workspace",
"hidden": true
},
"ng-add": {
"factory": "./src/schematics/init/init",
"schema": "./src/schematics/init/schema.json",
"description": "Convert an existing CLI project into an Nx Workspace",
"hidden": true
},
"preset": {
"factory": "./src/schematics/preset/preset",
"schema": "./src/schematics/preset/schema.json",
"description": "Create application in an empty workspace",
"hidden": true
},
"move": {
"factory": "./src/schematics/move/move",
"schema": "./src/schematics/move/schema.json",
"aliases": ["mv"],
"description": "Move an application or library to another folder"
},
"remove": {
"factory": "./src/schematics/remove/remove",
"schema": "./src/schematics/remove/schema.json",
"aliases": ["rm"],
"description": "Remove an application or library"
},
"new": {
"factory": "./src/schematics/new/new#newGenerator",
"schema": "./src/schematics/new/schema.json",
"description": "Create a workspace",
"hidden": true

View File

@ -0,0 +1,383 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`new --preset angular should generate an empty nx.json 1`] = `
Object {
"affected": Object {
"defaultBase": "master",
},
"implicitDependencies": Object {
".eslintrc.json": "*",
"nx.json": "*",
"package.json": Object {
"dependencies": "*",
"devDependencies": "*",
},
"tsconfig.base.json": "*",
"tslint.json": "*",
"workspace.json": "*",
},
"npmScope": "npmScope",
"projects": Object {},
"tasksRunnerOptions": Object {
"default": Object {
"options": Object {
"cacheableOperations": Array [
"build",
"lint",
"test",
"e2e",
],
},
"runner": "@nrwl/workspace/tasks-runners/default",
},
},
}
`;
exports[`new --preset angular should generate an empty workspace.json 1`] = `
Object {
"cli": Object {
"defaultCollection": "@nrwl/workspace",
},
"generators": Object {
"@nrwl/angular": Object {
"application": Object {
"linter": "eslint",
},
"library": Object {
"linter": "eslint",
},
"storybook-configuration": Object {
"linter": "eslint",
},
},
},
"projects": Object {},
"version": 2,
}
`;
exports[`new --preset angular should generate necessary npm dependencies 1`] = `
Object {
"dependencies": Object {
"@nrwl/angular": "*",
},
"devDependencies": Object {
"@nrwl/cli": "*",
"@nrwl/tao": "*",
"@nrwl/workspace": "*",
"@types/node": "12.12.38",
"dotenv": "6.2.0",
"eslint": "7.10.0",
"prettier": "2.1.2",
"ts-node": "~9.1.1",
"tslint": "~6.1.0",
"typescript": "~4.0.3",
},
"license": "MIT",
"name": "my-workspace",
"private": true,
"scripts": Object {
"affected": "nx affected",
"affected:apps": "nx affected:apps",
"affected:build": "nx affected:build",
"affected:dep-graph": "nx affected:dep-graph",
"affected:e2e": "nx affected:e2e",
"affected:libs": "nx affected:libs",
"affected:lint": "nx affected:lint",
"affected:test": "nx affected:test",
"build": "nx build",
"dep-graph": "nx dep-graph",
"e2e": "nx e2e",
"format": "nx format:write",
"format:check": "nx format:check",
"format:write": "nx format:write",
"help": "nx help",
"lint": "nx workspace-lint && nx lint",
"nx": "nx",
"start": "nx serve",
"test": "nx test",
"update": "nx migrate latest",
"workspace-generator": "nx workspace-generator",
},
"version": "0.0.0",
}
`;
exports[`new --preset empty should generate an empty nx.json 1`] = `
Object {
"affected": Object {
"defaultBase": "master",
},
"implicitDependencies": Object {
".eslintrc.json": "*",
"nx.json": "*",
"package.json": Object {
"dependencies": "*",
"devDependencies": "*",
},
"tsconfig.base.json": "*",
"tslint.json": "*",
"workspace.json": "*",
},
"npmScope": "npmScope",
"projects": Object {},
"tasksRunnerOptions": Object {
"default": Object {
"options": Object {
"cacheableOperations": Array [
"build",
"lint",
"test",
"e2e",
],
},
"runner": "@nrwl/workspace/tasks-runners/default",
},
},
}
`;
exports[`new --preset empty should generate an empty workspace.json 1`] = `
Object {
"cli": Object {
"defaultCollection": "@nrwl/workspace",
},
"projects": Object {},
"version": 2,
}
`;
exports[`new --preset empty should generate an necessary npm dependencies 1`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {
"@nrwl/cli": "*",
"@nrwl/tao": "*",
"@nrwl/workspace": "*",
"@types/node": "12.12.38",
"dotenv": "6.2.0",
"eslint": "7.10.0",
"prettier": "2.1.2",
"ts-node": "~9.1.1",
"tslint": "~6.1.0",
"typescript": "~4.0.3",
},
"license": "MIT",
"name": "my-workspace",
"private": true,
"scripts": Object {
"affected": "nx affected",
"affected:apps": "nx affected:apps",
"affected:build": "nx affected:build",
"affected:dep-graph": "nx affected:dep-graph",
"affected:e2e": "nx affected:e2e",
"affected:libs": "nx affected:libs",
"affected:lint": "nx affected:lint",
"affected:test": "nx affected:test",
"build": "nx build",
"dep-graph": "nx dep-graph",
"e2e": "nx e2e",
"format": "nx format:write",
"format:check": "nx format:check",
"format:write": "nx format:write",
"help": "nx help",
"lint": "nx workspace-lint && nx lint",
"nx": "nx",
"start": "nx serve",
"test": "nx test",
"update": "nx migrate latest",
"workspace-generator": "nx workspace-generator",
},
"version": "0.0.0",
}
`;
exports[`new --preset empty should generate necessary npm dependencies 1`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {
"@nrwl/cli": "*",
"@nrwl/tao": "*",
"@nrwl/workspace": "*",
"@types/node": "12.12.38",
"dotenv": "6.2.0",
"eslint": "7.10.0",
"prettier": "2.1.2",
"ts-node": "~9.1.1",
"tslint": "~6.1.0",
"typescript": "~4.0.3",
},
"license": "MIT",
"name": "my-workspace",
"private": true,
"scripts": Object {
"affected": "nx affected",
"affected:apps": "nx affected:apps",
"affected:build": "nx affected:build",
"affected:dep-graph": "nx affected:dep-graph",
"affected:e2e": "nx affected:e2e",
"affected:libs": "nx affected:libs",
"affected:lint": "nx affected:lint",
"affected:test": "nx affected:test",
"build": "nx build",
"dep-graph": "nx dep-graph",
"e2e": "nx e2e",
"format": "nx format:write",
"format:check": "nx format:check",
"format:write": "nx format:write",
"help": "nx help",
"lint": "nx workspace-lint && nx lint",
"nx": "nx",
"start": "nx serve",
"test": "nx test",
"update": "nx migrate latest",
"workspace-generator": "nx workspace-generator",
},
"version": "0.0.0",
}
`;
exports[`new --preset react should generate an empty nx.json 1`] = `
Object {
"affected": Object {
"defaultBase": "master",
},
"implicitDependencies": Object {
".eslintrc.json": "*",
"nx.json": "*",
"package.json": Object {
"dependencies": "*",
"devDependencies": "*",
},
"tsconfig.base.json": "*",
"tslint.json": "*",
"workspace.json": "*",
},
"npmScope": "npmScope",
"projects": Object {},
"tasksRunnerOptions": Object {
"default": Object {
"options": Object {
"cacheableOperations": Array [
"build",
"lint",
"test",
"e2e",
],
},
"runner": "@nrwl/workspace/tasks-runners/default",
},
},
}
`;
exports[`new --preset react should generate an empty workspace.json 1`] = `
Object {
"cli": Object {
"defaultCollection": "@nrwl/workspace",
},
"projects": Object {},
"version": 2,
}
`;
exports[`new --preset react should generate necessary npm dependencies 1`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {
"@nrwl/cli": "*",
"@nrwl/react": "*",
"@nrwl/tao": "*",
"@nrwl/workspace": "*",
"@types/node": "12.12.38",
"dotenv": "6.2.0",
"eslint": "7.10.0",
"prettier": "2.1.2",
"ts-node": "~9.1.1",
"tslint": "~6.1.0",
"typescript": "~4.0.3",
},
"license": "MIT",
"name": "my-workspace",
"private": true,
"scripts": Object {
"affected": "nx affected",
"affected:apps": "nx affected:apps",
"affected:build": "nx affected:build",
"affected:dep-graph": "nx affected:dep-graph",
"affected:e2e": "nx affected:e2e",
"affected:libs": "nx affected:libs",
"affected:lint": "nx affected:lint",
"affected:test": "nx affected:test",
"build": "nx build",
"dep-graph": "nx dep-graph",
"e2e": "nx e2e",
"format": "nx format:write",
"format:check": "nx format:check",
"format:write": "nx format:write",
"help": "nx help",
"lint": "nx workspace-lint && nx lint",
"nx": "nx",
"start": "nx serve",
"test": "nx test",
"update": "nx migrate latest",
"workspace-generator": "nx workspace-generator",
},
"version": "0.0.0",
}
`;
exports[`new should 1`] = `
Object {
"cli": Object {
"defaultCollection": "@nrwl/workspace",
},
"projects": Object {},
"version": 2,
}
`;
exports[`new should generate an empty nx.json 1`] = `
Object {
"affected": Object {
"defaultBase": "master",
},
"implicitDependencies": Object {
".eslintrc.json": "*",
"nx.json": "*",
"package.json": Object {
"dependencies": "*",
"devDependencies": "*",
},
"tsconfig.base.json": "*",
"tslint.json": "*",
"workspace.json": "*",
},
"npmScope": "npmScope",
"projects": Object {},
"tasksRunnerOptions": Object {
"default": Object {
"options": Object {
"cacheableOperations": Array [
"build",
"lint",
"test",
"e2e",
],
},
"runner": "@nrwl/workspace/tasks-runners/default",
},
},
}
`;
exports[`new should generate an empty workspace.json 1`] = `
Object {
"cli": Object {
"defaultCollection": "@nrwl/workspace",
},
"projects": Object {},
"version": 2,
}
`;

View File

@ -0,0 +1,65 @@
import { createTree } from '@nrwl/devkit/testing';
import { readJson, Tree } from '@nrwl/devkit';
import { newGenerator, Preset, Schema } from './new';
import { Linter } from '../../utils/lint';
const defaultOptions: Omit<Schema, 'name' | 'directory' | 'appName'> = {
cli: 'nx',
preset: Preset.Empty,
skipInstall: false,
skipGit: false,
linter: Linter.EsLint,
defaultBase: 'master',
};
describe('new', () => {
let tree: Tree;
beforeEach(() => {
tree = createTree();
});
it('should generate an empty workspace.json', async () => {
await newGenerator(tree, {
...defaultOptions,
name: 'my-workspace',
directory: 'my-workspace',
npmScope: 'npmScope',
appName: 'app',
});
expect(readJson(tree, 'my-workspace/workspace.json')).toMatchSnapshot();
});
it('should generate an empty nx.json', async () => {
await newGenerator(tree, {
...defaultOptions,
name: 'my-workspace',
directory: 'my-workspace',
npmScope: 'npmScope',
appName: 'app',
});
expect(readJson(tree, 'my-workspace/nx.json')).toMatchSnapshot();
});
describe('--preset', () => {
describe.each([[Preset.Empty], [Preset.Angular], [Preset.React]])(
'%s',
(preset) => {
beforeEach(async () => {
await newGenerator(tree, {
...defaultOptions,
name: 'my-workspace',
directory: 'my-workspace',
npmScope: 'npmScope',
appName: 'app',
preset,
});
});
it('should generate necessary npm dependencies', () => {
expect(readJson(tree, 'my-workspace/package.json')).toMatchSnapshot();
});
}
);
});
});

View File

@ -1,31 +1,21 @@
import {
chain,
move,
noop,
Rule,
schematic,
SchematicContext,
Tree,
} from '@angular-devkit/schematics';
import {
NodePackageInstallTask,
RepositoryInitializerTask,
} from '@angular-devkit/schematics/tasks';
formatFiles,
updateJson,
addDependenciesToPackageJson,
installPackagesTask,
getWorkspacePath,
convertNxGenerator,
names,
getPackageManagerCommand,
} from '@nrwl/devkit';
import {
addDepsToPackageJson,
updateWorkspaceInTree,
} from '../../utils/ast-utils';
import { formatFiles } from '../../utils/rules/format-files';
import { nxVersion } from '../../utils/versions';
import * as path from 'path';
import { Observable } from 'rxjs';
import { spawn } from 'child_process';
import { getPackageManagerCommand } from '@nrwl/tao/src/shared/package-manager';
import { join } from 'path';
import * as yargsParser from 'yargs-parser';
import { names } from '@nrwl/devkit';
import { spawn, SpawnOptions } from 'child_process';
import { workspaceGenerator } from '../workspace/workspace';
import { nxVersion } from '../../utils/versions';
export enum Preset {
Empty = 'empty',
@ -51,67 +41,114 @@ export interface Schema {
nxCloud?: boolean;
preset: Preset;
commit?: { name: string; email: string; message?: string };
defaultBase?: string;
nxWorkspaceRoot?: string;
defaultBase: string;
linter: 'tslint' | 'eslint';
packageManager?: string;
}
class RunPresetTask {
toConfiguration() {
return {
name: 'RunPreset',
};
}
}
function createPresetTaskExecutor(opts: Schema) {
function generatePreset(host: Tree, opts: Schema) {
const cliCommand = opts.cli === 'angular' ? 'ng' : 'nx';
const parsedArgs = yargsParser(process.argv, {
boolean: ['interactive'],
});
return {
name: 'RunPreset',
create: () => {
return Promise.resolve(() => {
const spawnOptions = {
stdio: [process.stdin, process.stdout, process.stderr],
shell: true,
cwd: path.join(opts.nxWorkspaceRoot || process.cwd(), opts.directory),
};
const pmc = getPackageManagerCommand();
const executable = `${pmc.exec} ${cliCommand}`;
const args = [
`g`,
`@nrwl/workspace:preset`,
`--name=${opts.appName}`,
opts.style ? `--style=${opts.style}` : null,
opts.linter ? `--linter=${opts.linter}` : null,
opts.npmScope
? `--npmScope=${opts.npmScope}`
: `--npmScope=${opts.name}`,
opts.preset ? `--preset=${opts.preset}` : null,
`--cli=${cliCommand}`,
parsedArgs.interactive ? '--interactive=true' : '--interactive=false',
].filter((e) => !!e);
return new Observable((obs) => {
spawn(executable, args, spawnOptions).on('close', (code: number) => {
if (code === 0) {
obs.next();
obs.complete();
} else {
const message = 'Workspace creation failed, see above.';
obs.error(new Error(message));
}
});
});
});
},
const spawnOptions = {
stdio: [process.stdin, process.stdout, process.stderr],
shell: true,
cwd: join(host.root, opts.directory),
};
const pmc = getPackageManagerCommand();
const executable = `${pmc.exec} ${cliCommand}`;
const args = [
`g`,
`@nrwl/workspace:preset`,
`--name=${opts.appName}`,
opts.style ? `--style=${opts.style}` : null,
opts.linter ? `--linter=${opts.linter}` : null,
opts.npmScope ? `--npmScope=${opts.npmScope}` : `--npmScope=${opts.name}`,
opts.preset ? `--preset=${opts.preset}` : null,
`--cli=${cliCommand}`,
parsedArgs.interactive ? '--interactive=true' : '--interactive=false',
].filter((e) => !!e);
return new Promise((resolve, reject) => {
spawn(executable, args, spawnOptions).on('close', (code: number) => {
if (code === 0) {
resolve();
} else {
const message = 'Workspace creation failed, see above.';
reject(new Error(message));
}
});
});
}
export default function (options: Schema): Rule {
async function initializeGitRepo(
host: Tree,
rootDirectory: string,
options: Schema
) {
const execute = (args: ReadonlyArray<string>, ignoreErrorStream = false) => {
const outputStream = 'ignore';
const errorStream = ignoreErrorStream ? 'ignore' : process.stderr;
const spawnOptions: SpawnOptions = {
stdio: [process.stdin, outputStream, errorStream],
shell: true,
cwd: join(host.root, rootDirectory),
env: {
...process.env,
...(options.commit.name
? {
GIT_AUTHOR_NAME: options.commit.name,
GIT_COMMITTER_NAME: options.commit.name,
}
: {}),
...(options.commit.email
? {
GIT_AUTHOR_EMAIL: options.commit.email,
GIT_COMMITTER_EMAIL: options.commit.email,
}
: {}),
},
};
return new Promise((resolve, reject) => {
spawn('git', args, spawnOptions).on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(code);
}
});
});
};
const hasCommand = await execute(['--version']).then(
() => true,
() => false
);
if (!hasCommand) {
return;
}
const insideRepo = await execute(
['rev-parse', '--is-inside-work-tree'],
true
).then(
() => true,
() => false
);
if (insideRepo) {
console.info(
`Directory is already under version control. Skipping initialization of git.`
);
return;
}
await execute(['init']);
await execute(['add', '.']);
if (options.commit) {
const message = options.commit.message || 'initial commit';
await execute(['commit', `-m "${message}"`]);
}
console.info('Successfully initialized git.');
}
export async function newGenerator(host: Tree, options: Schema) {
if (
options.skipInstall &&
options.preset !== 'empty' &&
@ -125,139 +162,97 @@ export default function (options: Schema): Rule {
options = normalizeOptions(options);
const layout = options.preset === 'oss' ? 'packages' : 'apps-and-libs';
const layout: 'packages' | 'apps-and-libs' =
options.preset === 'oss' ? 'packages' : 'apps-and-libs';
const workspaceOpts = {
...options,
layout,
preset: undefined,
nxCloud: undefined,
};
return (host: Tree, context: SchematicContext) => {
const engineHost = (context.engine.workflow as any).engineHost;
engineHost.registerTaskExecutor(createPresetTaskExecutor(options));
workspaceGenerator(host, workspaceOpts);
return chain([
schematic('workspace', workspaceOpts),
options.cli === 'angular' ? setDefaultPackageManager(options) : noop(),
setDefaultLinter(options),
addPresetDependencies(options),
addCloudDependencies(options),
move('/', options.directory),
addTasks(options),
formatFiles({ skipFormat: false }, options.directory),
])(Tree.empty(), context);
if (options.cli === 'angular') {
setDefaultPackageManager(host, options);
}
setDefaultLinter(host, options);
addPresetDependencies(host, options);
addCloudDependencies(host, options);
await formatFiles(host);
host.listChanges().forEach((change) => {
if (change.type !== 'DELETE') {
host.rename(change.path, join(options.directory, change.path));
}
});
return async () => {
installPackagesTask(host, false, options.directory);
await generatePreset(host, options);
if (!options.skipGit) {
await initializeGitRepo(host, options.directory, options);
}
};
}
function addCloudDependencies(options: Schema) {
return options.nxCloud
? addDepsToPackageJson({}, { '@nrwl/nx-cloud': 'latest' }, false)
: noop();
}
export default newGenerator;
export const newSchematic = convertNxGenerator(newGenerator);
function addPresetDependencies(options: Schema) {
if (options.preset === 'empty') {
return noop();
} else if (options.preset === 'web-components') {
return addDepsToPackageJson(
function addCloudDependencies(host: Tree, options: Schema) {
if (options.nxCloud) {
return addDependenciesToPackageJson(
host,
{},
{
'@nrwl/web': nxVersion,
},
false
{ '@nrwl/nx-cloud': 'latest' }
);
} else if (options.preset === 'angular') {
return addDepsToPackageJson(
{
'@nrwl/angular': nxVersion,
},
{},
false
);
} else if (options.preset === 'angular-nest') {
return addDepsToPackageJson(
{
'@nrwl/angular': nxVersion,
},
{
'@nrwl/nest': nxVersion,
},
false
);
} else if (options.preset === 'react') {
return addDepsToPackageJson(
{},
{
'@nrwl/react': nxVersion,
},
false
);
} else if (options.preset === 'react-express') {
return addDepsToPackageJson(
{},
{
'@nrwl/react': nxVersion,
'@nrwl/express': nxVersion,
},
false
);
} else if (options.preset === 'next') {
return addDepsToPackageJson(
{},
{
'@nrwl/next': nxVersion,
},
false
);
} else if (options.preset === 'nest') {
return addDepsToPackageJson(
{},
{
'@nrwl/nest': nxVersion,
},
false
);
} else {
return noop();
}
}
function addTasks(options: Schema) {
return (host: Tree, context: SchematicContext) => {
let packageTask;
let presetInstallTask;
if (!options.skipInstall) {
packageTask = context.addTask(
new NodePackageInstallTask(options.directory)
);
}
if (options.preset !== 'empty') {
const createPresetTask = context.addTask(new RunPresetTask(), [
packageTask,
]);
const presetDependencies: Omit<
Record<
Preset,
{ dependencies: Record<string, string>; dev: Record<string, string> }
>,
Preset.Empty | Preset.OSS
> = {
[Preset.WebComponents]: { dependencies: {}, dev: { '@nrwl/web': nxVersion } },
[Preset.Angular]: { dependencies: { '@nrwl/angular': nxVersion }, dev: {} },
[Preset.AngularWithNest]: {
dependencies: { '@nrwl/angular': nxVersion },
dev: { '@nrwl/nest': nxVersion },
},
[Preset.React]: {
dependencies: {},
dev: {
'@nrwl/react': nxVersion,
},
},
[Preset.ReactWithExpress]: {
dependencies: {},
dev: {
'@nrwl/react': nxVersion,
'@nrwl/express': nxVersion,
},
},
[Preset.Nest]: {
dependencies: {},
dev: {
'@nrwl/nest': nxVersion,
},
},
[Preset.NextJs]: {
dependencies: {},
dev: {
'@nrwl/next': nxVersion,
},
},
};
presetInstallTask = context.addTask(
new NodePackageInstallTask(options.directory),
[createPresetTask]
);
}
if (!options.skipGit) {
const commit =
typeof options.commit == 'object'
? options.commit
: !!options.commit
? {}
: false;
context.addTask(
new RepositoryInitializerTask(options.directory, commit),
presetInstallTask
? [presetInstallTask]
: packageTask
? [packageTask]
: []
);
}
};
function addPresetDependencies(host: Tree, options: Schema) {
if (options.preset === Preset.Empty || options.preset === Preset.OSS) {
return;
}
const { dependencies, dev } = presetDependencies[options.preset];
return addDependenciesToPackageJson(host, dependencies, dev);
}
function normalizeOptions(options: Schema): Schema {
@ -269,30 +264,29 @@ function normalizeOptions(options: Schema): Schema {
return options;
}
function setDefaultLinter({ linter, preset }: Schema): Rule {
function setDefaultLinter(host: Tree, { linter, preset }: Schema) {
// Don't do anything if someone doesn't pick angular
if (preset === 'angular' || preset === 'angular-nest') {
switch (linter) {
case 'eslint': {
return setESLintDefault();
}
case 'tslint': {
return setTSLintDefault();
}
default: {
return noop();
}
if (preset !== 'angular' && preset !== 'angular-nest') {
return;
}
switch (linter) {
case 'eslint': {
setESLintDefault(host);
break;
}
case 'tslint': {
setTSLintDefault(host);
break;
}
} else {
return noop();
}
}
/**
* This sets ESLint as the default for any schematics that default to TSLint
*/
function setESLintDefault() {
return updateWorkspaceInTree((json) => {
function setESLintDefault(host: Tree) {
updateJson(host, getWorkspacePath(host), (json) => {
setDefault(json, '@nrwl/angular', 'application', 'linter', 'eslint');
setDefault(json, '@nrwl/angular', 'library', 'linter', 'eslint');
setDefault(
@ -309,8 +303,8 @@ function setESLintDefault() {
/**
* This sets TSLint as the default for any schematics that default to ESLint
*/
function setTSLintDefault() {
return updateWorkspaceInTree((json) => {
function setTSLintDefault(host: Tree) {
updateJson(host, getWorkspacePath(host), (json) => {
setDefault(json, '@nrwl/workspace', 'library', 'linter', 'tslint');
setDefault(json, '@nrwl/cypress', 'cypress-project', 'linter', 'tslint');
setDefault(json, '@nrwl/cypress', 'cypress-project', 'linter', 'tslint');
@ -325,12 +319,12 @@ function setTSLintDefault() {
});
}
function setDefaultPackageManager({ packageManager }: Schema) {
function setDefaultPackageManager(host: Tree, { packageManager }: Schema) {
if (!packageManager) {
return noop();
return;
}
return updateWorkspaceInTree((json) => {
updateJson(host, getWorkspacePath(host), (json) => {
if (!json.cli) {
json.cli = {};
}

View File

@ -3,6 +3,7 @@
"id": "NxWorkspaceNew",
"title": "Create an empty workspace",
"type": "object",
"cli": "nx",
"properties": {
"name": {
"description": "The name of the workspace.",
@ -13,17 +14,17 @@
},
"x-prompt": "What name would you like to use for the workspace?"
},
"cli": {
"description": "CLI used for generating code and running tasks",
"type": "string",
"enum": ["nx", "angular"],
"default": "nx"
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
"default": "css"
},
"directory": {
"type": "string",
"format": "path",
"description": "The directory name to create the workspace in.",
"default": ""
},
"npmScope": {
"type": "string",
"description": "Npm scope for importing libs."
@ -82,11 +83,6 @@
"type": "boolean",
"default": false
},
"nxWorkspaceRoot": {
"type": "string",
"description": "Root directory.",
"hidden": true
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`workspace should create a prettierrc file 1`] = `
"{
\\"singleQuote\\": true
}"
`;

View File

@ -1,6 +1,6 @@
<% if(cli === 'angular') { %>
# <%= utils.classify(name) %>
# <%= formattedNames.className %>
This project was generated using [Nx](https://nx.dev).
@ -92,7 +92,7 @@ Visit the [Nx Documentation](https://nx.dev/angular) to learn more.
<% } else { %>
# <%= utils.classify(name) %>
# <%= formattedNames.className %>
This project was generated using [Nx](https://nx.dev).

View File

@ -1 +0,0 @@
<%= defaultNrwlPrettierConfig %>

View File

@ -1,5 +1,5 @@
{
"name": "<%= utils.dasherize(name) %>",
"name": "<%= formattedNames.fileName %>",
"version": "0.0.0",
"license": "MIT",
"scripts": {

View File

@ -1,5 +1,4 @@
export interface Schema {
directory: string;
name: string;
npmScope?: string;
skipInstall?: boolean;
@ -8,5 +7,5 @@ export interface Schema {
commit?: { name: string; email: string; message?: string };
cli: 'nx' | 'angular';
layout: 'apps-and-libs' | 'packages';
defaultBase?: string;
defaultBase: string;
}

View File

@ -1,6 +1,7 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsNxNgNew",
"cli": "nx",
"title": "Create an empty workspace",
"type": "object",
"properties": {
@ -41,6 +42,7 @@
"type": "string",
"format": "path",
"description": "The directory name to create the workspace in.",
"x-deprecated": "This option is no longer used.",
"default": ""
},
"layout": {

View File

@ -1,16 +1,22 @@
import { Tree } from '@angular-devkit/schematics';
import { NxJson, readJsonInTree } from '@nrwl/workspace';
import { runSchematic } from '../../utils/testing';
import { readJson, Tree } from '@nrwl/devkit';
import { workspaceGenerator } from './workspace';
import { createTree } from '@nrwl/devkit/testing';
import { NxJson } from '../../core/shared-interfaces';
describe('workspace', () => {
let projectTree: Tree;
describe('@nrwl/workspace:workspace', () => {
let tree: Tree;
beforeEach(() => {
projectTree = Tree.empty();
tree = createTree();
});
it('should create files', async () => {
const tree = await runSchematic('workspace', { name: 'proj' }, projectTree);
workspaceGenerator(tree, {
name: 'proj',
cli: 'nx',
layout: 'apps-and-libs',
defaultBase: 'main',
});
expect(tree.exists('/nx.json')).toBe(true);
expect(tree.exists('/workspace.json')).toBe(true);
expect(tree.exists('/.prettierrc')).toBe(true);
@ -18,8 +24,13 @@ describe('workspace', () => {
});
it('should create nx.json', async () => {
const tree = await runSchematic('workspace', { name: 'proj' }, projectTree);
const nxJson = readJsonInTree<NxJson>(tree, '/nx.json');
workspaceGenerator(tree, {
name: 'proj',
cli: 'nx',
layout: 'apps-and-libs',
defaultBase: 'master',
});
const nxJson = readJson<NxJson>(tree, '/nx.json');
expect(nxJson).toEqual({
npmScope: 'proj',
affected: {
@ -48,9 +59,24 @@ describe('workspace', () => {
});
});
it('should create a prettierrc file', async () => {
workspaceGenerator(tree, {
name: 'proj',
cli: 'nx',
layout: 'apps-and-libs',
defaultBase: 'main',
});
expect(tree.read('.prettierrc').toString()).toMatchSnapshot();
});
it('should recommend vscode extensions', async () => {
const tree = await runSchematic('workspace', { name: 'proj' }, projectTree);
const recommendations = readJsonInTree<{ recommendations: string[] }>(
workspaceGenerator(tree, {
name: 'proj',
cli: 'nx',
layout: 'apps-and-libs',
defaultBase: 'main',
});
const recommendations = readJson<{ recommendations: string[] }>(
tree,
'/.vscode/extensions.json'
).recommendations;
@ -62,12 +88,13 @@ describe('workspace', () => {
});
it('should recommend vscode extensions (angular)', async () => {
const tree = await runSchematic(
'workspace',
{ name: 'proj', cli: 'angular' },
projectTree
);
const recommendations = readJsonInTree<{ recommendations: string[] }>(
workspaceGenerator(tree, {
name: 'proj',
cli: 'angular',
layout: 'apps-and-libs',
defaultBase: 'main',
});
const recommendations = readJson<{ recommendations: string[] }>(
tree,
'/.vscode/extensions.json'
).recommendations;
@ -81,39 +108,42 @@ describe('workspace', () => {
});
it('should add decorate-angular-cli when used with angular cli', async () => {
const tree = await runSchematic(
'workspace',
{ name: 'proj', cli: 'angular' },
projectTree
);
workspaceGenerator(tree, {
name: 'proj',
cli: 'angular',
layout: 'apps-and-libs',
defaultBase: 'main',
});
expect(tree.exists('/decorate-angular-cli.js')).toBe(true);
const packageJson = readJsonInTree(tree, '/package.json');
const packageJson = readJson(tree, '/package.json');
expect(packageJson.scripts.postinstall).toEqual(
'node ./decorate-angular-cli.js'
);
});
it('should not add decorate-angular-cli when used with nx cli', async () => {
const tree = await runSchematic(
'workspace',
{ name: 'proj', cli: 'nx' },
projectTree
);
workspaceGenerator(tree, {
name: 'proj',
cli: 'nx',
layout: 'apps-and-libs',
defaultBase: 'main',
});
expect(tree.exists('/decorate-angular-cli.js')).toBe(false);
const packageJson = readJsonInTree(tree, '/package.json');
const packageJson = readJson(tree, '/package.json');
expect(packageJson.scripts.postinstall).toBeUndefined();
});
it('should create a workspace using package layout', async () => {
const tree = await runSchematic(
'workspace',
{ name: 'proj', cli: 'nx', layout: 'packages' },
projectTree
);
workspaceGenerator(tree, {
name: 'proj',
cli: 'nx',
layout: 'packages',
defaultBase: 'main',
});
expect(tree.exists('/packages/.gitkeep')).toBe(true);
expect(tree.exists('/apps/.gitkeep')).toBe(false);
expect(tree.exists('/libs/.gitkeep')).toBe(false);
const nx = readJsonInTree(tree, '/nx.json');
const nx = readJson(tree, '/nx.json');
expect(nx.workspaceLayout).toEqual({
appsDir: 'packages',
libsDir: 'packages',

View File

@ -1,17 +1,12 @@
import {
apply,
branchAndMerge,
chain,
mergeWith,
noop,
Rule,
SchematicContext,
template,
generateFiles,
Tree,
url,
} from '@angular-devkit/schematics';
updateJson,
convertNxGenerator,
names,
writeJson,
} from '@nrwl/devkit';
import { Schema } from './schema';
import { join, strings } from '@angular-devkit/core';
import {
angularCliVersion,
eslintVersion,
@ -21,7 +16,6 @@ import {
} from '../../utils/versions';
import { readFileSync } from 'fs';
import { join as pathJoin } from 'path';
import { updateJsonInTree } from '@nrwl/workspace';
export const DEFAULT_NRWL_PRETTIER_CONFIG = {
singleQuote: true,
@ -31,11 +25,11 @@ const decorateAngularClI = (host: Tree) => {
const decorateCli = readFileSync(
pathJoin(__dirname as any, '..', 'utils', 'decorate-angular-cli.js__tmpl__')
).toString();
host.create('decorate-angular-cli.js', decorateCli);
host.write('decorate-angular-cli.js', decorateCli);
};
function setWorkspaceLayoutProperties(options: Schema) {
return updateJsonInTree('nx.json', (json) => {
function setWorkspaceLayoutProperties(tree: Tree, options: Schema) {
updateJson(tree, 'nx.json', (json) => {
if (options.layout === 'packages') {
json.workspaceLayout = {
appsDir: 'packages',
@ -46,56 +40,51 @@ function setWorkspaceLayoutProperties(options: Schema) {
});
}
function createAppsAndLibsFolders(options: Schema) {
return (host: Tree) => {
if (options.layout === 'packages') {
host.create('packages/.gitkeep', '');
} else {
host.create('apps/.gitkeep', '');
host.create('libs/.gitkeep', '');
}
};
function createAppsAndLibsFolders(host: Tree, options: Schema) {
if (options.layout === 'packages') {
host.write('packages/.gitkeep', '');
} else {
host.write('apps/.gitkeep', '');
host.write('libs/.gitkeep', '');
}
}
export default function (options: Schema): Rule {
function createFiles(host: Tree, options: Schema) {
const npmScope = options.npmScope ? options.npmScope : options.name;
const formattedNames = names(options.name);
generateFiles(host, pathJoin(__dirname, './files'), '', {
formattedNames,
dot: '.',
tmpl: '',
workspaceFile: options.cli === 'angular' ? 'angular' : 'workspace',
cliCommand: options.cli === 'angular' ? 'ng' : 'nx',
nxCli: false,
typescriptVersion,
prettierVersion,
eslintVersion,
// angular cli is used only when workspace schematics is added to angular cli
angularCliVersion,
...(options as object),
nxVersion,
npmScope,
});
}
function createPrettierrc(host: Tree) {
writeJson(host, '.prettierrc', DEFAULT_NRWL_PRETTIER_CONFIG);
}
export function workspaceGenerator(host: Tree, options: Schema) {
if (!options.name) {
throw new Error(`Invalid options, "name" is required.`);
}
return (host: Tree, context: SchematicContext) => {
const npmScope = options.npmScope ? options.npmScope : options.name;
const templateSource = apply(url('./files'), [
template({
utils: strings,
dot: '.',
tmpl: '',
workspaceFile: options.cli === 'angular' ? 'angular' : 'workspace',
cliCommand: options.cli === 'angular' ? 'ng' : 'nx',
nxCli: false,
typescriptVersion,
prettierVersion,
eslintVersion,
// angular cli is used only when workspace schematics is added to angular cli
angularCliVersion,
...(options as object),
nxVersion,
npmScope,
defaultNrwlPrettierConfig: JSON.stringify(
DEFAULT_NRWL_PRETTIER_CONFIG,
null,
2
),
}),
]);
return chain([
branchAndMerge(
chain([
mergeWith(templateSource),
options.cli === 'angular' ? decorateAngularClI : noop(),
setWorkspaceLayoutProperties(options),
createAppsAndLibsFolders(options),
])
),
])(host, context);
};
createFiles(host, options);
createPrettierrc(host);
if (options.cli === 'angular') {
decorateAngularClI(host);
}
setWorkspaceLayoutProperties(host, options);
createAppsAndLibsFolders(host, options);
}
export const workspaceSchematic = convertNxGenerator(workspaceGenerator);