Skip to content

Commit

Permalink
feat(nx): add presets to simplify the creation of workspaces with ang…
Browse files Browse the repository at this point in the history
…ular and fullstack apps
  • Loading branch information
vsavkin committed Feb 25, 2019
1 parent a6fd16d commit 7f2c16f
Show file tree
Hide file tree
Showing 35 changed files with 517 additions and 172 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ node_modules
packages/schematics/src/collection/**/files/*.json
/.vscode
/.idea
/.github
2 changes: 1 addition & 1 deletion e2e/schematics/ng-new.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { toClassName } from '@nrwl/schematics/src/utils/name-utils';

describe('Nrwl Workspace', () => {
fit('should work', async () => {
it('should work', async () => {
ensureProject();
const myapp = uniq('myapp');
const mylib = uniq('mylib');
Expand Down
12 changes: 6 additions & 6 deletions e2e/schematics/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as http from 'http';
import * as path from 'path';
import * as treeKill from 'tree-kill';

function getData() {
function getData(): Promise<any> {
return new Promise(resolve => {
http.get('http://localhost:3333/api', res => {
expect(res.statusCode).toEqual(200);
Expand All @@ -23,7 +23,7 @@ function getData() {
data += chunk;
});
res.once('end', () => {
resolve(data);
resolve(JSON.parse(data));
});
});
});
Expand Down Expand Up @@ -77,7 +77,7 @@ describe('Node Applications', () => {
expect(data.toString()).toContain('Listening at http://localhost:3333');
const result = await getData();

expect(result).toEqual(`Welcome to ${nodeapp}!`);
expect(result.message).toEqual(`Welcome to ${nodeapp}!`);
treeKill(server.pid, 'SIGTERM', err => {
expect(err).toBeFalsy();
resolve();
Expand Down Expand Up @@ -118,7 +118,7 @@ describe('Node Applications', () => {
}

const result = await getData();
expect(result).toEqual(`Welcome to ${nodeapp}!`);
expect(result.message).toEqual(`Welcome to ${nodeapp}!`);
treeKill(process.pid, 'SIGTERM', err => {
expect(collectedOutput.startsWith('DONE')).toBeTruthy();
expect(err).toBeFalsy();
Expand Down Expand Up @@ -164,7 +164,7 @@ describe('Node Applications', () => {
if (message.includes('Listening at http://localhost:3333')) {
const result = await getData();

expect(result).toEqual(`Welcome to ${nestapp}!`);
expect(result.message).toEqual(`Welcome to ${nestapp}!`);
treeKill(server.pid, 'SIGTERM', err => {
expect(err).toBeFalsy();
resolve();
Expand All @@ -186,7 +186,7 @@ describe('Node Applications', () => {
return;
}
const result = await getData();
expect(result).toEqual(`Welcome to ${nestapp}!`);
expect(result.message).toEqual(`Welcome to ${nestapp}!`);
treeKill(process.pid, 'SIGTERM', err => {
expect(err).toBeFalsy();
done();
Expand Down
3 changes: 2 additions & 1 deletion packages/schematics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"tmp": "0.0.33",
"viz.js": "^1.8.1",
"yargs-parser": "10.0.0",
"yargs": "^11.0.0"
"yargs": "^11.0.0",
"prettier": "1.15.3"
}
}
9 changes: 8 additions & 1 deletion packages/schematics/src/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@
"ng-new": {
"factory": "./collection/ng-new",
"schema": "./collection/ng-new/schema.json",
"description": "Create an empty workspace"
"description": "Create a workspace"
},

"workspace": {
"factory": "./collection/workspace",
"schema": "./collection/workspace/schema.json",
"description": "Create an empty workspace",
"hidden": true
},

"application": {
Expand Down
240 changes: 192 additions & 48 deletions packages/schematics/src/collection/ng-new/index.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,217 @@
import {
apply,
branchAndMerge,
chain,
mergeWith,
move,
noop,
Rule,
schematic,
SchematicContext,
template,
Tree,
url
Tree
} from '@angular-devkit/schematics';
import { Schema } from './schema';
import { strings } from '@angular-devkit/core';
import { addImportToModule, insert } from '../../utils/ast-utils';
import * as ts from 'typescript';
import { insertImport } from '@schematics/angular/utility/ast-utils';
import {
NodePackageInstallTask,
RepositoryInitializerTask
} from '@angular-devkit/schematics/tasks';
import { libVersions } from '../../lib-versions';
import { DEFAULT_NRWL_PRETTIER_CONFIG } from '../../utils/common';

export default function(options: Schema): Rule {
if (!options.name) {
throw new Error(`Invalid options, "name" is required.`);
}
if (!options.directory) {
options.directory = options.name;
}

const workspaceOpts = { ...options, preset: undefined };
return (host: Tree, context: SchematicContext) => {
addTasks(options, context);
const npmScope = options.npmScope ? options.npmScope : options.name;
const templateSource = apply(url('./files'), [
template({
utils: strings,
dot: '.',
tmpl: '',
...libVersions,
...(options as object),
npmScope,
defaultNrwlPrettierConfig: JSON.stringify(
DEFAULT_NRWL_PRETTIER_CONFIG,
null,
2
)
})
]);
return chain([branchAndMerge(chain([mergeWith(templateSource)]))])(
host,
context
);
return chain([
schematic('workspace', workspaceOpts),
createPreset(options),
move('/', options.directory),
addTasks(options)
])(Tree.empty(), context);
};
}

function addTasks(options: Schema, context: SchematicContext) {
let packageTask;
if (!options.skipInstall) {
packageTask = context.addTask(
new NodePackageInstallTask(options.directory)
function createPreset(options: Schema): Rule {
if (options.preset === 'empty') {
return noop();
} else if (options.preset === 'angular') {
return chain([
schematic(
'application',
{ name: options.name, style: options.style },
{ interactive: false }
)
]);
} else {
return chain([
schematic(
'application',
{ name: options.name, style: options.style },
{ interactive: false }
),
schematic(
'node-application',
{
name: 'api',
frontendProject: options.name
},
{ interactive: false }
),
schematic(
'library',
{ name: 'api-interface', framework: 'none' },
{ interactive: false }
),
connectFrontendAndApi(options)
]);
}
}

function connectFrontendAndApi(options: Schema) {
return (host: Tree) => {
host.create(
'libs/api-interface/src/lib/interfaces.ts',
`export interface Message { message: string }`
);
host.overwrite(
'libs/api-interface/src/index.ts',
`export * from './lib/interfaces';`
);

const modulePath = `apps/${options.name}/src/app/app.module.ts`;
const moduleFile = ts.createSourceFile(
modulePath,
host.read(modulePath).toString(),
ts.ScriptTarget.Latest,
true
);
insert(host, modulePath, [
insertImport(
moduleFile,
modulePath,
'HttpClientModule',
`@angular/common/http`
),
...addImportToModule(
moduleFile,
`@angular/common/http`,
`HttpClientModule`
)
]);

const scope = options.npmScope ? options.npmScope : options.name;
const style = options.style ? options.style : 'css';
host.overwrite(
`apps/${options.name}/src/app/app.component.ts`,
`import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Message } from '@${scope}/api-interface';
@Component({
selector: '${scope}-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.${style}']
})
export class AppComponent {
hello$ = this.http.get<Message>('/api/hello')
constructor(private http: HttpClient) {}
}
`
);

host.overwrite(
`apps/${options.name}/src/app/app.component.spec.ts`,
`import { Component } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [HttpClientModule]
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
});
`
);

host.overwrite(
`apps/${options.name}/src/app/app.component.html`,
`<div style="text-align:center">
<h1>Welcome to ${options.name}!</h1>
<img
width="300"
src="https://raw.githubusercontent.com/nrwl/nx/master/nx-logo.png"
/>
</div>
<div>Message: {{ (hello$|async)|json }}</div>
`
);

host.overwrite(
`apps/api/src/app/app.controller.ts`,
`import { Controller, Get } from '@nestjs/common';
import { Message } from '@${scope}/api-interface';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello')
getData(): Message {
return this.appService.getData();
}
if (!options.skipGit) {
const commit =
typeof options.commit == 'object'
? options.commit
: !!options.commit
? {}
: false;
context.addTask(
new RepositoryInitializerTask(options.directory, commit),
packageTask ? [packageTask] : []
}
`
);

host.overwrite(
`apps/api/src/app/app.service.ts`,
`import { Injectable } from '@nestjs/common';
import { Message } from '@${scope}/api-interface';
@Injectable()
export class AppService {
getData(): Message {
return { message: 'Welcome to api!' };
}
}
`
);
};
}

function addTasks(options: Schema) {
return (host: Tree, context: SchematicContext) => {
let packageTask;
if (!options.skipInstall) {
packageTask = context.addTask(
new NodePackageInstallTask(options.directory)
);
}
if (!options.skipGit) {
const commit =
typeof options.commit == 'object'
? options.commit
: !!options.commit
? {}
: false;
context.addTask(
new RepositoryInitializerTask(options.directory, commit),
packageTask ? [packageTask] : []
);
}
};
}
Loading

0 comments on commit 7f2c16f

Please sign in to comment.