feat(node): migrate @nrwl/node schematics to devkit (#4958)

This commit is contained in:
Jason Jean 2021-03-11 14:18:22 -05:00 committed by GitHub
parent aba5c44ec1
commit 4df3b66152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 790 additions and 844 deletions

View File

@ -71,7 +71,7 @@
}, },
"node": { "node": {
"tags": [], "tags": [],
"implicitDependencies": ["workspace", "jest"] "implicitDependencies": ["workspace", "jest", "linter"]
}, },
"next": { "next": {
"tags": [], "tags": [],

View File

@ -75,7 +75,7 @@ export function generateFiles(
newContent = fs.readFileSync(filePath); newContent = fs.readFileSync(filePath);
} else { } else {
const template = fs.readFileSync(filePath).toString(); const template = fs.readFileSync(filePath).toString();
newContent = ejs.render(template, substitutions); newContent = ejs.render(template, substitutions, {});
} }
host.write(computedPath, newContent); host.write(computedPath, newContent);

View File

@ -46,7 +46,9 @@ describe('app', () => {
); );
expect(eslintrcJson).toMatchInlineSnapshot(` expect(eslintrcJson).toMatchInlineSnapshot(`
Object { Object {
"extends": "../../.eslintrc.json", "extends": Array [
"../../.eslintrc.json",
],
"ignorePatterns": Array [ "ignorePatterns": Array [
"!**/*", "!**/*",
], ],

View File

@ -24,7 +24,9 @@ describe('app', () => {
); );
expect(eslintrcJson).toMatchInlineSnapshot(` expect(eslintrcJson).toMatchInlineSnapshot(`
Object { Object {
"extends": "../../.eslintrc.json", "extends": Array [
"../../.eslintrc.json",
],
"ignorePatterns": Array [ "ignorePatterns": Array [
"!**/*", "!**/*",
], ],

View File

@ -2,23 +2,44 @@
"name": "nx/node", "name": "nx/node",
"version": "0.1", "version": "0.1",
"extends": ["@nrwl/workspace"], "extends": ["@nrwl/workspace"],
"schematics": { "generators": {
"init": { "init": {
"factory": "./src/schematics/init/init", "factory": "./src/generators/init/init",
"schema": "./src/schematics/init/schema.json", "schema": "./src/generators/init/schema.json",
"description": "Initialize the @nrwl/node plugin", "description": "Initialize the @nrwl/node plugin",
"aliases": ["ng-add"], "aliases": ["ng-add"],
"hidden": true "hidden": true
}, },
"application": { "application": {
"factory": "./src/schematics/application/application", "factory": "./src/generators/application/application",
"schema": "./src/schematics/application/schema.json", "schema": "./src/generators/application/schema.json",
"aliases": ["app"], "aliases": ["app"],
"description": "Create a node application" "description": "Create a node application"
}, },
"library": { "library": {
"factory": "./src/schematics/library/library", "factory": "./src/generators/library/library",
"schema": "./src/schematics/library/schema.json", "schema": "./src/generators/library/schema.json",
"aliases": ["lib"],
"description": "Create a library"
}
},
"schematics": {
"init": {
"factory": "./src/generators/init/init#initSchematic",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the @nrwl/node plugin",
"aliases": ["ng-add"],
"hidden": true
},
"application": {
"factory": "./src/generators/application/application#applicationSchematic",
"schema": "./src/generators/application/schema.json",
"aliases": ["app"],
"description": "Create a node application"
},
"library": {
"factory": "./src/generators/library/library#librarySchematic",
"schema": "./src/generators/library/schema.json",
"aliases": ["lib"], "aliases": ["lib"],
"description": "Create a library" "description": "Create a library"
} }

View File

@ -1,2 +1,2 @@
export { applicationGenerator } from './src/schematics/application/application'; export { applicationGenerator } from './src/generators/application/application';
export { libraryGenerator } from './src/schematics/library/library'; export { libraryGenerator } from './src/generators/library/library';

View File

@ -29,6 +29,7 @@
"migrations": "./migrations.json" "migrations": "./migrations.json"
}, },
"dependencies": { "dependencies": {
"@nrwl/workspace": "*",
"@nrwl/devkit": "*", "@nrwl/devkit": "*",
"@nrwl/jest": "*", "@nrwl/jest": "*",
"@nrwl/linter": "*", "@nrwl/linter": "*",

View File

@ -1,25 +1,37 @@
import { Tree } from '@angular-devkit/schematics'; import { NxJsonConfiguration, readJson, Tree } from '@nrwl/devkit';
import { createEmptyWorkspace } from '@nrwl/workspace/testing'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { runSchematic } from '../../utils/testing';
import { NxJson, readJsonInTree } from '@nrwl/workspace';
// to break the dependency
const createApp = require('../../../../angular/' + 'src/utils/testing')
.createApp;
import { applicationGenerator as angularApplicationGenerator } from '@nrwl/angular/generators';
import { Schema } from './schema'; import { Schema } from './schema';
import { applicationGenerator } from './application';
import { overrideCollectionResolutionForTesting } from '@nrwl/devkit/ngcli-adapter';
import { join } from 'path';
describe('app', () => { describe('app', () => {
let appTree: Tree; let tree: Tree;
beforeEach(() => { beforeEach(() => {
appTree = Tree.empty(); tree = createTreeWithEmptyWorkspace();
appTree = createEmptyWorkspace(appTree);
overrideCollectionResolutionForTesting({
'@nrwl/cypress': join(__dirname, '../../../../cypress/collection.json'),
'@nrwl/jest': join(__dirname, '../../../../jest/collection.json'),
'@nrwl/workspace': join(
__dirname,
'../../../../workspace/collection.json'
),
'@nrwl/angular': join(__dirname, '../../../../angular/collection.json'),
});
});
afterEach(() => {
overrideCollectionResolutionForTesting(null);
}); });
describe('not nested', () => { describe('not nested', () => {
it('should update workspace.json', async () => { it('should update workspace.json', async () => {
const tree = await runSchematic('app', { name: 'myNodeApp' }, appTree); await applicationGenerator(tree, { name: 'myNodeApp' });
const workspaceJson = readJsonInTree(tree, '/workspace.json'); const workspaceJson = readJson(tree, '/workspace.json');
const project = workspaceJson.projects['my-node-app']; const project = workspaceJson.projects['my-node-app'];
expect(project.root).toEqual('apps/my-node-app'); expect(project.root).toEqual('apps/my-node-app');
expect(project.architect).toEqual( expect(project.architect).toEqual(
@ -67,12 +79,8 @@ describe('app', () => {
}); });
it('should update nx.json', async () => { it('should update nx.json', async () => {
const tree = await runSchematic( await applicationGenerator(tree, { name: 'myNodeApp', tags: 'one,two' });
'app', const nxJson = readJson<NxJsonConfiguration>(tree, '/nx.json');
{ name: 'myNodeApp', tags: 'one,two' },
appTree
);
const nxJson = readJsonInTree<NxJson>(tree, '/nx.json');
expect(nxJson.projects).toEqual({ expect(nxJson.projects).toEqual({
'my-node-app': { 'my-node-app': {
tags: ['one', 'two'], tags: ['one', 'two'],
@ -81,11 +89,11 @@ describe('app', () => {
}); });
it('should generate files', async () => { it('should generate files', async () => {
const tree = await runSchematic('app', { name: 'myNodeApp' }, appTree); await applicationGenerator(tree, { name: 'myNodeApp' });
expect(tree.exists(`apps/my-node-app/jest.config.js`)).toBeTruthy(); expect(tree.exists(`apps/my-node-app/jest.config.js`)).toBeTruthy();
expect(tree.exists('apps/my-node-app/src/main.ts')).toBeTruthy(); expect(tree.exists('apps/my-node-app/src/main.ts')).toBeTruthy();
const tsconfig = readJsonInTree(tree, 'apps/my-node-app/tsconfig.json'); const tsconfig = readJson(tree, 'apps/my-node-app/tsconfig.json');
expect(tsconfig).toMatchInlineSnapshot(` expect(tsconfig).toMatchInlineSnapshot(`
Object { Object {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
@ -102,17 +110,16 @@ describe('app', () => {
} }
`); `);
const tsconfigApp = readJsonInTree( const tsconfigApp = readJson(tree, 'apps/my-node-app/tsconfig.app.json');
tree,
'apps/my-node-app/tsconfig.app.json'
);
expect(tsconfigApp.compilerOptions.outDir).toEqual('../../dist/out-tsc'); expect(tsconfigApp.compilerOptions.outDir).toEqual('../../dist/out-tsc');
expect(tsconfigApp.extends).toEqual('./tsconfig.json'); expect(tsconfigApp.extends).toEqual('./tsconfig.json');
const eslintrc = readJsonInTree(tree, 'apps/my-node-app/.eslintrc.json'); const eslintrc = readJson(tree, 'apps/my-node-app/.eslintrc.json');
expect(eslintrc).toMatchInlineSnapshot(` expect(eslintrc).toMatchInlineSnapshot(`
Object { Object {
"extends": "../../.eslintrc.json", "extends": Array [
"../../.eslintrc.json",
],
"ignorePatterns": Array [ "ignorePatterns": Array [
"!**/*", "!**/*",
], ],
@ -153,12 +160,11 @@ describe('app', () => {
describe('nested', () => { describe('nested', () => {
it('should update workspace.json', async () => { it('should update workspace.json', async () => {
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', directory: 'myDir' }, directory: 'myDir',
appTree });
); const workspaceJson = readJson(tree, '/workspace.json');
const workspaceJson = readJsonInTree(tree, '/workspace.json');
expect(workspaceJson.projects['my-dir-my-node-app'].root).toEqual( expect(workspaceJson.projects['my-dir-my-node-app'].root).toEqual(
'apps/my-dir/my-node-app' 'apps/my-dir/my-node-app'
@ -178,12 +184,12 @@ describe('app', () => {
}); });
it('should update nx.json', async () => { it('should update nx.json', async () => {
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', directory: 'myDir', tags: 'one,two' }, directory: 'myDir',
appTree tags: 'one,two',
); });
const nxJson = readJsonInTree<NxJson>(tree, '/nx.json'); const nxJson = readJson<NxJsonConfiguration>(tree, '/nx.json');
expect(nxJson.projects).toEqual({ expect(nxJson.projects).toEqual({
'my-dir-my-node-app': { 'my-dir-my-node-app': {
tags: ['one', 'two'], tags: ['one', 'two'],
@ -193,15 +199,14 @@ describe('app', () => {
it('should generate files', async () => { it('should generate files', async () => {
const hasJsonValue = ({ path, expectedValue, lookupFn }) => { const hasJsonValue = ({ path, expectedValue, lookupFn }) => {
const config = readJsonInTree(tree, path); const config = readJson(tree, path);
expect(lookupFn(config)).toEqual(expectedValue); expect(lookupFn(config)).toEqual(expectedValue);
}; };
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', directory: 'myDir' }, directory: 'myDir',
appTree });
);
// Make sure these exist // Make sure these exist
[ [
@ -226,7 +231,7 @@ describe('app', () => {
{ {
path: 'apps/my-dir/my-node-app/.eslintrc.json', path: 'apps/my-dir/my-node-app/.eslintrc.json',
lookupFn: (json) => json.extends, lookupFn: (json) => json.extends,
expectedValue: '../../../.eslintrc.json', expectedValue: ['../../../.eslintrc.json'],
}, },
].forEach(hasJsonValue); ].forEach(hasJsonValue);
}); });
@ -234,17 +239,16 @@ describe('app', () => {
describe('--unit-test-runner none', () => { describe('--unit-test-runner none', () => {
it('should not generate test configuration', async () => { it('should not generate test configuration', async () => {
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', unitTestRunner: 'none' }, unitTestRunner: 'none',
appTree });
);
expect(tree.exists('jest.config.js')).toBeFalsy(); expect(tree.exists('jest.config.js')).toBeFalsy();
expect(tree.exists('apps/my-node-app/src/test-setup.ts')).toBeFalsy(); expect(tree.exists('apps/my-node-app/src/test-setup.ts')).toBeFalsy();
expect(tree.exists('apps/my-node-app/src/test.ts')).toBeFalsy(); expect(tree.exists('apps/my-node-app/src/test.ts')).toBeFalsy();
expect(tree.exists('apps/my-node-app/tsconfig.spec.json')).toBeFalsy(); expect(tree.exists('apps/my-node-app/tsconfig.spec.json')).toBeFalsy();
expect(tree.exists('apps/my-node-app/jest.config.js')).toBeFalsy(); expect(tree.exists('apps/my-node-app/jest.config.js')).toBeFalsy();
const workspaceJson = readJsonInTree(tree, 'workspace.json'); const workspaceJson = readJson(tree, 'workspace.json');
expect( expect(
workspaceJson.projects['my-node-app'].architect.test workspaceJson.projects['my-node-app'].architect.test
).toBeUndefined(); ).toBeUndefined();
@ -264,59 +268,53 @@ describe('app', () => {
describe('--frontendProject', () => { describe('--frontendProject', () => {
it('should configure proxy', async () => { it('should configure proxy', async () => {
appTree = createApp(appTree, 'my-frontend'); await angularApplicationGenerator(tree, { name: 'my-frontend' });
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', frontendProject: 'my-frontend' }, frontendProject: 'my-frontend',
appTree });
);
expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy(); expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy();
const serve = readJsonInTree(tree, 'workspace.json').projects[ const serve = readJson(tree, 'workspace.json').projects['my-frontend']
'my-frontend' .architect.serve;
].architect.serve;
expect(serve.options.proxyConfig).toEqual( expect(serve.options.proxyConfig).toEqual(
'apps/my-frontend/proxy.conf.json' 'apps/my-frontend/proxy.conf.json'
); );
}); });
it('should configure proxies for multiple node projects with the same frontend app', async () => { it('should configure proxies for multiple node projects with the same frontend app', async () => {
appTree = createApp(appTree, 'my-frontend'); await angularApplicationGenerator(tree, { name: 'my-frontend' });
appTree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'cart',
{ name: 'cart', frontendProject: 'my-frontend' }, frontendProject: 'my-frontend',
appTree });
);
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'billing',
{ name: 'billing', frontendProject: 'my-frontend' }, frontendProject: 'my-frontend',
appTree });
);
expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy(); expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy();
expect(readJsonInTree(tree, 'apps/my-frontend/proxy.conf.json')).toEqual({ expect(readJson(tree, 'apps/my-frontend/proxy.conf.json')).toEqual({
'/api': { target: 'http://localhost:3333', secure: false }, '/api': { target: 'http://localhost:3333', secure: false },
'/billing-api': { target: 'http://localhost:3333', secure: false }, '/billing-api': { target: 'http://localhost:3333', secure: false },
}); });
}); });
it('should work with unnormalized project names', async () => { it('should work with unnormalized project names', async () => {
appTree = createApp(appTree, 'myFrontend'); await angularApplicationGenerator(tree, { name: 'myFrontend' });
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', frontendProject: 'myFrontend' }, frontendProject: 'myFrontend',
appTree });
);
expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy(); expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy();
const serve = readJsonInTree(tree, 'workspace.json').projects[ const serve = readJson(tree, 'workspace.json').projects['my-frontend']
'my-frontend' .architect.serve;
].architect.serve;
expect(serve.options.proxyConfig).toEqual( expect(serve.options.proxyConfig).toEqual(
'apps/my-frontend/proxy.conf.json' 'apps/my-frontend/proxy.conf.json'
); );
@ -325,22 +323,22 @@ describe('app', () => {
describe('--babelJest', () => { describe('--babelJest', () => {
it('should use babel for jest', async () => { it('should use babel for jest', async () => {
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', tags: 'one,two', babelJest: true } as Schema, tags: 'one,two',
appTree babelJest: true,
); } as Schema);
expect(tree.readContent(`apps/my-node-app/jest.config.js`)) expect(tree.read(`apps/my-node-app/jest.config.js`).toString())
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"module.exports = { "module.exports = {
displayName: 'my-node-app', displayName: 'my-node-app',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
transform: { transform: {
'^.+\\\\\\\\.[tj]s$': 'babel-jest', '^.+\\\\\\\\.[tj]s$': 'babel-jest'
}, },
moduleFileExtensions: ['ts', 'js', 'html'], moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/my-node-app', coverageDirectory: '../../coverage/apps/my-node-app'
}; };
" "
`); `);
@ -348,38 +346,30 @@ describe('app', () => {
}); });
describe('--js flag', () => { describe('--js flag', () => {
it('should generate js files instead of ts files', async () => { it('should generate js files instead of ts files', async () => {
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ js: true,
name: 'myNodeApp', } as Schema);
js: true,
} as Schema,
appTree
);
expect(tree.exists(`apps/my-node-app/jest.config.js`)).toBeTruthy(); expect(tree.exists(`apps/my-node-app/jest.config.js`)).toBeTruthy();
expect(tree.exists('apps/my-node-app/src/main.js')).toBeTruthy(); expect(tree.exists('apps/my-node-app/src/main.js')).toBeTruthy();
const tsConfig = readJsonInTree(tree, 'apps/my-node-app/tsconfig.json'); const tsConfig = readJson(tree, 'apps/my-node-app/tsconfig.json');
expect(tsConfig.compilerOptions).toEqual({ expect(tsConfig.compilerOptions).toEqual({
allowJs: true, allowJs: true,
}); });
const tsConfigApp = readJsonInTree( const tsConfigApp = readJson(tree, 'apps/my-node-app/tsconfig.app.json');
tree,
'apps/my-node-app/tsconfig.app.json'
);
expect(tsConfigApp.include).toEqual(['**/*.ts', '**/*.js']); expect(tsConfigApp.include).toEqual(['**/*.ts', '**/*.js']);
expect(tsConfigApp.exclude).toEqual(['**/*.spec.ts', '**/*.spec.js']); expect(tsConfigApp.exclude).toEqual(['**/*.spec.ts', '**/*.spec.js']);
}); });
it('should update workspace.json', async () => { it('should update workspace.json', async () => {
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', js: true } as Schema, js: true,
appTree } as Schema);
); const workspaceJson = readJson(tree, '/workspace.json');
const workspaceJson = readJsonInTree(tree, '/workspace.json');
const project = workspaceJson.projects['my-node-app']; const project = workspaceJson.projects['my-node-app'];
const buildTarget = project.architect.build; const buildTarget = project.architect.build;
@ -393,11 +383,11 @@ describe('app', () => {
}); });
it('should generate js files for nested libs as well', async () => { it('should generate js files for nested libs as well', async () => {
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', directory: 'myDir', js: true } as Schema, directory: 'myDir',
appTree js: true,
); } as Schema);
expect( expect(
tree.exists(`apps/my-dir/my-node-app/jest.config.js`) tree.exists(`apps/my-dir/my-node-app/jest.config.js`)
).toBeTruthy(); ).toBeTruthy();
@ -407,11 +397,10 @@ describe('app', () => {
describe('--pascalCaseFiles', () => { describe('--pascalCaseFiles', () => {
it(`should notify that this flag doesn't do anything`, async () => { it(`should notify that this flag doesn't do anything`, async () => {
const tree = await runSchematic( await applicationGenerator(tree, {
'app', name: 'myNodeApp',
{ name: 'myNodeApp', pascalCaseFiles: true } as Schema, pascalCaseFiles: true,
appTree } as Schema);
);
// @TODO how to spy on context ? // @TODO how to spy on context ?
// expect(contextLoggerSpy).toHaveBeenCalledWith('NOTE: --pascalCaseFiles is a noop') // expect(contextLoggerSpy).toHaveBeenCalledWith('NOTE: --pascalCaseFiles is a noop')

View File

@ -0,0 +1,240 @@
import {
addProjectConfiguration,
convertNxGenerator,
formatFiles,
generateFiles,
GeneratorCallback,
getWorkspaceLayout,
joinPathFragments,
logger,
names,
NxJsonProjectConfiguration,
offsetFromRoot,
ProjectConfiguration,
readProjectConfiguration,
readWorkspaceConfiguration,
TargetConfiguration,
toJS,
Tree,
updateProjectConfiguration,
updateTsConfigsToJs,
updateWorkspaceConfiguration,
} from '@nrwl/devkit';
import { join } from 'path';
import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { jestProjectGenerator } from '@nrwl/jest';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { Schema } from './schema';
import { initGenerator } from '../init/init';
interface NormalizedSchema extends Schema {
appProjectRoot: string;
parsedTags: string[];
}
function getBuildConfig(
project: ProjectConfiguration,
options: NormalizedSchema
): TargetConfiguration {
return {
executor: '@nrwl/node:build',
outputs: ['{options.outputPath}'],
options: {
outputPath: joinPathFragments('dist', options.appProjectRoot),
main: joinPathFragments(
project.sourceRoot,
'main' + (options.js ? '.js' : '.ts')
),
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
assets: [joinPathFragments(project.sourceRoot, 'assets')],
},
configurations: {
production: {
optimization: true,
extractLicenses: true,
inspect: false,
fileReplacements: [
{
replace: joinPathFragments(
project.sourceRoot,
'environments/environment' + (options.js ? '.js' : '.ts')
),
with: joinPathFragments(
project.sourceRoot,
'environments/environment.prod' + (options.js ? '.js' : '.ts')
),
},
],
},
},
};
}
function getServeConfig(options: NormalizedSchema): TargetConfiguration {
return {
executor: '@nrwl/node:execute',
options: {
buildTarget: `${options.name}:build`,
},
};
}
function addProject(tree: Tree, options: NormalizedSchema) {
const project: ProjectConfiguration & NxJsonProjectConfiguration = {
root: options.appProjectRoot,
sourceRoot: join(options.appProjectRoot, 'src'),
projectType: 'application',
targets: {},
tags: options.parsedTags,
};
project.targets.build = getBuildConfig(project, options);
project.targets.serve = getServeConfig(options);
addProjectConfiguration(tree, options.name, project);
const workspace = readWorkspaceConfiguration(tree);
if (!workspace.defaultProject) {
workspace.defaultProject = options.name;
updateWorkspaceConfiguration(tree, workspace);
}
}
function addAppFiles(tree: Tree, options: NormalizedSchema) {
generateFiles(tree, join(__dirname, './files/app'), options.appProjectRoot, {
tmpl: '',
name: options.name,
root: options.appProjectRoot,
offset: offsetFromRoot(options.appProjectRoot),
});
if (options.js) {
toJS(tree);
}
if (options.pascalCaseFiles) {
logger.warn('NOTE: --pascalCaseFiles is a noop');
}
}
function addProxy(tree: Tree, options: NormalizedSchema) {
const projectConfig = readProjectConfiguration(tree, options.frontendProject);
if (projectConfig.targets && projectConfig.targets.serve) {
const pathToProxyFile = `${projectConfig.root}/proxy.conf.json`;
projectConfig.targets.serve.options.proxyConfig = pathToProxyFile;
if (!tree.exists(pathToProxyFile)) {
tree.write(
pathToProxyFile,
JSON.stringify(
{
'/api': {
target: 'http://localhost:3333',
secure: false,
},
},
null,
2
)
);
} else {
//add new entry to existing config
const proxyFileContent = tree.read(pathToProxyFile).toString();
const proxyModified = {
...JSON.parse(proxyFileContent),
[`/${options.name}-api`]: {
target: 'http://localhost:3333',
secure: false,
},
};
tree.write(pathToProxyFile, JSON.stringify(proxyModified, null, 2));
}
updateProjectConfiguration(tree, options.frontendProject, projectConfig);
}
}
export async function applicationGenerator(tree: Tree, schema: Schema) {
const options = normalizeOptions(tree, schema);
const tasks: GeneratorCallback[] = [];
const initTask = await initGenerator(tree, {
...options,
skipFormat: true,
});
tasks.push(initTask);
addAppFiles(tree, options);
addProject(tree, options);
if (options.linter !== Linter.None) {
const lintTask = await lintProjectGenerator(tree, {
linter: options.linter,
project: options.name,
tsConfigPaths: [
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
],
eslintFilePatterns: [`${options.appProjectRoot}/**/*.ts`],
skipFormat: true,
});
tasks.push(lintTask);
}
if (options.unitTestRunner === 'jest') {
const jestTask = await jestProjectGenerator(tree, {
project: options.name,
setupFile: 'none',
skipSerializers: true,
supportTsx: options.js,
babelJest: options.babelJest,
});
tasks.push(jestTask);
}
if (options.js) {
updateTsConfigsToJs(tree, { projectRoot: options.appProjectRoot });
}
if (options.frontendProject) {
addProxy(tree, options);
}
if (!options.skipFormat) {
await formatFiles(tree);
}
return runTasksInSerial(...tasks);
}
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
const { appsDir } = getWorkspaceLayout(host);
const appDirectory = options.directory
? `${names(options.directory).fileName}/${names(options.name).fileName}`
: names(options.name).fileName;
const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-');
const appProjectRoot = joinPathFragments(appsDir, appDirectory);
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
return {
...options,
name: names(appProjectName).fileName,
frontendProject: options.frontendProject
? names(options.frontendProject).fileName
: undefined,
appProjectRoot,
parsedTags,
linter: options.linter ?? Linter.EsLint,
unitTestRunner: options.unitTestRunner ?? 'jest',
};
}
export default applicationGenerator;
export const applicationSchematic = convertNxGenerator(applicationGenerator);

View File

@ -0,0 +1,15 @@
import { Linter } from '@nrwl/linter';
export interface Schema {
name: string;
skipFormat?: boolean;
skipPackageJson?: boolean;
directory?: string;
unitTestRunner?: 'jest' | 'none';
linter?: Linter;
tags?: string;
frontendProject?: string;
babelJest?: boolean;
js?: boolean;
pascalCaseFiles?: boolean;
}

View File

@ -1,5 +1,6 @@
{ {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"cli": "nx",
"id": "SchematicsNxNodeApp", "id": "SchematicsNxNodeApp",
"title": "Nx Application Options Schema", "title": "Nx Application Options Schema",
"type": "object", "type": "object",

View File

@ -0,0 +1,49 @@
import { addDependenciesToPackageJson, readJson, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { nxVersion } from '../../utils/versions';
import { initGenerator } from './init';
describe('init', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should add dependencies', async () => {
const existing = 'existing';
const existingVersion = '1.0.0';
addDependenciesToPackageJson(
tree,
{
'@nrwl/node': nxVersion,
[existing]: existingVersion,
},
{
[existing]: existingVersion,
}
);
await initGenerator(tree, {});
const packageJson = readJson(tree, 'package.json');
expect(packageJson.dependencies['@nrwl/node']).toBeUndefined();
expect(packageJson.dependencies[existing]).toBeDefined();
expect(packageJson.devDependencies['@nrwl/node']).toBeDefined();
expect(packageJson.devDependencies[existing]).toBeDefined();
});
describe('defaultCollection', () => {
it('should be set if none was set before', async () => {
await initGenerator(tree, {});
const workspaceJson = readJson(tree, 'workspace.json');
expect(workspaceJson.cli.defaultCollection).toEqual('@nrwl/node');
});
});
it('should not add jest config if unitTestRunner is none', async () => {
await initGenerator(tree, { unitTestRunner: 'none' });
expect(tree.exists('jest.config.js')).toEqual(false);
});
});

View File

@ -0,0 +1,53 @@
import {
addDependenciesToPackageJson,
convertNxGenerator,
formatFiles,
GeneratorCallback,
Tree,
updateJson,
} from '@nrwl/devkit';
import { nxVersion } from '../../utils/versions';
import { Schema } from './schema';
import { setDefaultCollection } from '@nrwl/workspace/src/utilities/set-default-collection';
import { jestInitGenerator } from '@nrwl/jest';
function updateDependencies(tree: Tree) {
updateJson(tree, 'package.json', (json) => {
delete json.dependencies['@nrwl/node'];
return json;
});
return addDependenciesToPackageJson(tree, {}, { '@nrwl/node': nxVersion });
}
function normalizeOptions(schema: Schema) {
return {
...schema,
unitTestRunner: schema.unitTestRunner ?? 'jest',
};
}
export async function initGenerator(tree: Tree, schema: Schema) {
const options = normalizeOptions(schema);
setDefaultCollection(tree, '@nrwl/node');
let jestInstall: GeneratorCallback;
if (options.unitTestRunner === 'jest') {
jestInstall = await jestInitGenerator(tree, {});
}
const installTask = await updateDependencies(tree);
if (!options.skipFormat) {
await formatFiles(tree);
}
return async () => {
if (jestInstall) {
await jestInstall();
}
await installTask();
};
}
export default initGenerator;
export const initSchematic = convertNxGenerator(initGenerator);

View File

@ -0,0 +1,4 @@
export interface Schema {
unitTestRunner?: 'jest' | 'none';
skipFormat?: boolean;
}

View File

@ -1,5 +1,6 @@
{ {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"cli": "nx",
"id": "NxNodeInit", "id": "NxNodeInit",
"title": "Init Node Plugin", "title": "Init Node Plugin",
"type": "object", "type": "object",

View File

@ -1,21 +1,20 @@
import { Tree } from '@angular-devkit/schematics'; import { NxJsonConfiguration, readJson, Tree } from '@nrwl/devkit';
import { NxJson, readJsonInTree } from '@nrwl/workspace'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { runSchematic } from '../../utils/testing';
import { Schema } from './schema.d'; import { Schema } from './schema.d';
import { libraryGenerator } from './library';
describe('lib', () => { describe('lib', () => {
let appTree: Tree; let tree: Tree;
beforeEach(() => { beforeEach(() => {
appTree = Tree.empty(); tree = createTreeWithEmptyWorkspace();
appTree = createEmptyWorkspace(appTree);
}); });
describe('not nested', () => { describe('not nested', () => {
it('should update workspace.json', async () => { it('should update workspace.json', async () => {
const tree = await runSchematic('lib', { name: 'myLib' }, appTree); await libraryGenerator(tree, { name: 'myLib' });
const workspaceJson = readJsonInTree(tree, '/workspace.json'); const workspaceJson = readJson(tree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib'); expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
expect(workspaceJson.projects['my-lib'].architect.build).toBeUndefined(); expect(workspaceJson.projects['my-lib'].architect.build).toBeUndefined();
expect(workspaceJson.projects['my-lib'].architect.lint).toEqual({ expect(workspaceJson.projects['my-lib'].architect.lint).toEqual({
@ -35,12 +34,12 @@ describe('lib', () => {
}); });
it('adds srcRootForCompilationRoot in workspace.json', async () => { it('adds srcRootForCompilationRoot in workspace.json', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ name: 'myLib', rootDir: './src', buildable: true }, rootDir: './src',
appTree buildable: true,
); });
const workspaceJson = readJsonInTree(tree, '/workspace.json'); const workspaceJson = readJson(tree, '/workspace.json');
expect( expect(
workspaceJson.projects['my-lib'].architect.build.options workspaceJson.projects['my-lib'].architect.build.options
.srcRootForCompilationRoot .srcRootForCompilationRoot
@ -48,12 +47,8 @@ describe('lib', () => {
}); });
it('should update nx.json', async () => { it('should update nx.json', async () => {
const tree = await runSchematic( await libraryGenerator(tree, { name: 'myLib', tags: 'one,two' });
'lib', const nxJson = readJson<NxJsonConfiguration>(tree, '/nx.json');
{ name: 'myLib', tags: 'one,two' },
appTree
);
const nxJson = readJsonInTree<NxJson>(tree, '/nx.json');
expect(nxJson.projects).toEqual({ expect(nxJson.projects).toEqual({
'my-lib': { 'my-lib': {
tags: ['one', 'two'], tags: ['one', 'two'],
@ -62,16 +57,16 @@ describe('lib', () => {
}); });
it('should update root tsconfig.base.json', async () => { it('should update root tsconfig.base.json', async () => {
const tree = await runSchematic('lib', { name: 'myLib' }, appTree); await libraryGenerator(tree, { name: 'myLib' });
const tsconfigJson = readJsonInTree(tree, '/tsconfig.base.json'); const tsconfigJson = readJson(tree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
'libs/my-lib/src/index.ts', 'libs/my-lib/src/index.ts',
]); ]);
}); });
it('should create a local tsconfig.json', async () => { it('should create a local tsconfig.json', async () => {
const tree = await runSchematic('lib', { name: 'myLib' }, appTree); await libraryGenerator(tree, { name: 'myLib' });
const tsconfigJson = readJsonInTree(tree, 'libs/my-lib/tsconfig.json'); const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.json');
expect(tsconfigJson).toMatchInlineSnapshot(` expect(tsconfigJson).toMatchInlineSnapshot(`
Object { Object {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
@ -90,30 +85,24 @@ describe('lib', () => {
}); });
it('should extend the local tsconfig.json with tsconfig.spec.json', async () => { it('should extend the local tsconfig.json with tsconfig.spec.json', async () => {
const tree = await runSchematic('lib', { name: 'myLib' }, appTree); await libraryGenerator(tree, { name: 'myLib' });
const tsconfigJson = readJsonInTree( const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.spec.json');
tree,
'libs/my-lib/tsconfig.spec.json'
);
expect(tsconfigJson.extends).toEqual('./tsconfig.json'); expect(tsconfigJson.extends).toEqual('./tsconfig.json');
}); });
it('should extend the local tsconfig.json with tsconfig.lib.json', async () => { it('should extend the local tsconfig.json with tsconfig.lib.json', async () => {
const tree = await runSchematic('lib', { name: 'myLib' }, appTree); await libraryGenerator(tree, { name: 'myLib' });
const tsconfigJson = readJsonInTree( const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.lib.json');
tree,
'libs/my-lib/tsconfig.lib.json'
);
expect(tsconfigJson.compilerOptions.types).toContain('node'); expect(tsconfigJson.compilerOptions.types).toContain('node');
expect(tsconfigJson.extends).toEqual('./tsconfig.json'); expect(tsconfigJson.extends).toEqual('./tsconfig.json');
}); });
it('should generate files', async () => { it('should generate files', async () => {
const tree = await runSchematic('lib', { name: 'myLib' }, appTree); await libraryGenerator(tree, { name: 'myLib' });
expect(tree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy(); expect(tree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy();
expect(tree.exists('libs/my-lib/src/index.ts')).toBeTruthy(); expect(tree.exists('libs/my-lib/src/index.ts')).toBeTruthy();
const eslintrc = readJsonInTree(tree, 'libs/my-lib/.eslintrc.json'); const eslintrc = readJson(tree, 'libs/my-lib/.eslintrc.json');
expect(eslintrc).toMatchInlineSnapshot(` expect(eslintrc).toMatchInlineSnapshot(`
Object { Object {
"extends": Array [ "extends": Array [
@ -159,32 +148,24 @@ describe('lib', () => {
describe('nested', () => { describe('nested', () => {
it('should update nx.json', async () => { it('should update nx.json', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ directory: 'myDir',
name: 'myLib', tags: 'one',
directory: 'myDir', });
tags: 'one', const nxJson = readJson<NxJsonConfiguration>(tree, '/nx.json');
},
appTree
);
const nxJson = readJsonInTree<NxJson>(tree, '/nx.json');
expect(nxJson.projects).toEqual({ expect(nxJson.projects).toEqual({
'my-dir-my-lib': { 'my-dir-my-lib': {
tags: ['one'], tags: ['one'],
}, },
}); });
const tree2 = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib2',
{ directory: 'myDir',
name: 'myLib2', tags: 'one,two',
directory: 'myDir', });
tags: 'one,two', const nxJson2 = readJson<NxJsonConfiguration>(tree, '/nx.json');
},
tree
);
const nxJson2 = readJsonInTree<NxJson>(tree2, '/nx.json');
expect(nxJson2.projects).toEqual({ expect(nxJson2.projects).toEqual({
'my-dir-my-lib': { 'my-dir-my-lib': {
tags: ['one'], tags: ['one'],
@ -196,22 +177,14 @@ describe('lib', () => {
}); });
it('should generate files', async () => { it('should generate files', async () => {
const tree = await runSchematic( await libraryGenerator(tree, { name: 'myLib', directory: 'myDir' });
'lib',
{ name: 'myLib', directory: 'myDir' },
appTree
);
expect(tree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy(); expect(tree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy();
expect(tree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy(); expect(tree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy();
}); });
it('should update workspace.json', async () => { it('should update workspace.json', async () => {
const tree = await runSchematic( await libraryGenerator(tree, { name: 'myLib', directory: 'myDir' });
'lib', const workspaceJson = readJson(tree, '/workspace.json');
{ name: 'myLib', directory: 'myDir' },
appTree
);
const workspaceJson = readJsonInTree(tree, '/workspace.json');
expect(workspaceJson.projects['my-dir-my-lib'].root).toEqual( expect(workspaceJson.projects['my-dir-my-lib'].root).toEqual(
'libs/my-dir/my-lib' 'libs/my-dir/my-lib'
@ -225,12 +198,8 @@ describe('lib', () => {
}); });
it('should update tsconfig.json', async () => { it('should update tsconfig.json', async () => {
const tree = await runSchematic( await libraryGenerator(tree, { name: 'myLib', directory: 'myDir' });
'lib', const tsconfigJson = readJson(tree, '/tsconfig.base.json');
{ name: 'myLib', directory: 'myDir' },
appTree
);
const tsconfigJson = readJsonInTree(tree, '/tsconfig.base.json');
expect( expect(
tsconfigJson.compilerOptions.paths['@proj/my-dir/my-lib'] tsconfigJson.compilerOptions.paths['@proj/my-dir/my-lib']
).toEqual(['libs/my-dir/my-lib/src/index.ts']); ).toEqual(['libs/my-dir/my-lib/src/index.ts']);
@ -243,15 +212,11 @@ describe('lib', () => {
expect.assertions(1); expect.assertions(1);
try { try {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ directory: 'myDir',
name: 'myLib', publishable: true,
directory: 'myDir', });
publishable: true,
},
appTree
);
} catch (e) { } catch (e) {
expect(e.message).toContain( expect(e.message).toContain(
'For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)' 'For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)'
@ -260,16 +225,9 @@ describe('lib', () => {
}); });
it('should create a local tsconfig.json', async () => { it('should create a local tsconfig.json', async () => {
const tree = await runSchematic( await libraryGenerator(tree, { name: 'myLib', directory: 'myDir' });
'lib',
{ name: 'myLib', directory: 'myDir' },
appTree
);
const tsconfigJson = readJsonInTree( const tsconfigJson = readJson(tree, 'libs/my-dir/my-lib/tsconfig.json');
tree,
'libs/my-dir/my-lib/tsconfig.json'
);
expect(tsconfigJson.extends).toEqual('../../../tsconfig.base.json'); expect(tsconfigJson.extends).toEqual('../../../tsconfig.base.json');
expect(tsconfigJson.references).toEqual([ expect(tsconfigJson.references).toEqual([
{ {
@ -284,20 +242,13 @@ describe('lib', () => {
describe('--unit-test-runner none', () => { describe('--unit-test-runner none', () => {
it('should not generate test configuration', async () => { it('should not generate test configuration', async () => {
const resultTree = await runSchematic( await libraryGenerator(tree, { name: 'myLib', unitTestRunner: 'none' });
'lib', expect(tree.exists('libs/my-lib/tsconfig.spec.json')).toBeFalsy();
{ name: 'myLib', unitTestRunner: 'none' }, expect(tree.exists('libs/my-lib/jest.config.js')).toBeFalsy();
appTree expect(tree.exists('libs/my-lib/lib/my-lib.spec.ts')).toBeFalsy();
); const workspaceJson = readJson(tree, 'workspace.json');
expect(resultTree.exists('libs/my-lib/tsconfig.spec.json')).toBeFalsy();
expect(resultTree.exists('libs/my-lib/jest.config.js')).toBeFalsy();
expect(resultTree.exists('libs/my-lib/lib/my-lib.spec.ts')).toBeFalsy();
const workspaceJson = readJsonInTree(resultTree, 'workspace.json');
expect(workspaceJson.projects['my-lib'].architect.test).toBeUndefined(); expect(workspaceJson.projects['my-lib'].architect.test).toBeUndefined();
const tsconfigJson = readJsonInTree( const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.json');
resultTree,
'libs/my-lib/tsconfig.json'
);
expect(tsconfigJson.extends).toEqual('../../tsconfig.base.json'); expect(tsconfigJson.extends).toEqual('../../tsconfig.base.json');
expect(tsconfigJson.references).toEqual([ expect(tsconfigJson.references).toEqual([
{ {
@ -320,12 +271,8 @@ describe('lib', () => {
describe('buildable package', () => { describe('buildable package', () => {
it('should have a builder defined', async () => { it('should have a builder defined', async () => {
const tree = await runSchematic( await libraryGenerator(tree, { name: 'myLib', buildable: true });
'lib', const workspaceJson = readJson(tree, '/workspace.json');
{ name: 'myLib', buildable: true },
appTree
);
const workspaceJson = readJsonInTree(tree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib'); expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
@ -352,12 +299,12 @@ describe('lib', () => {
describe('publishable package', () => { describe('publishable package', () => {
it('should have a builder defined', async () => { it('should have a builder defined', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ name: 'myLib', publishable: true, importPath: '@proj/mylib' }, publishable: true,
appTree importPath: '@proj/mylib',
); });
const workspaceJson = readJsonInTree(tree, '/workspace.json'); const workspaceJson = readJson(tree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib'); expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
@ -365,16 +312,13 @@ describe('lib', () => {
}); });
it('should update package.json', async () => { it('should update package.json', async () => {
const publishableTree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'mylib',
{ name: 'mylib', publishable: true, importPath: '@proj/mylib' }, publishable: true,
appTree importPath: '@proj/mylib',
); });
let packageJsonContent = readJsonInTree( let packageJsonContent = readJson(tree, 'libs/mylib/package.json');
publishableTree,
'libs/mylib/package.json'
);
expect(packageJsonContent.name).toEqual('@proj/mylib'); expect(packageJsonContent.name).toEqual('@proj/mylib');
}); });
@ -382,21 +326,14 @@ describe('lib', () => {
describe('--importPath', () => { describe('--importPath', () => {
it('should update the package.json & tsconfig with the given import path', async () => { it('should update the package.json & tsconfig with the given import path', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ publishable: true,
name: 'myLib', directory: 'myDir',
publishable: true, importPath: '@myorg/lib',
directory: 'myDir', });
importPath: '@myorg/lib', const packageJson = readJson(tree, 'libs/my-dir/my-lib/package.json');
}, const tsconfigJson = readJson(tree, '/tsconfig.base.json');
appTree
);
const packageJson = readJsonInTree(
tree,
'libs/my-dir/my-lib/package.json'
);
const tsconfigJson = readJsonInTree(tree, '/tsconfig.base.json');
expect(packageJson.name).toBe('@myorg/lib'); expect(packageJson.name).toBe('@myorg/lib');
expect( expect(
@ -405,26 +342,18 @@ describe('lib', () => {
}); });
it('should fail if the same importPath has already been used', async () => { it('should fail if the same importPath has already been used', async () => {
const tree1 = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib1',
{ publishable: true,
name: 'myLib1', importPath: '@myorg/lib',
publishable: true, });
importPath: '@myorg/lib',
},
appTree
);
try { try {
await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib2',
{ publishable: true,
name: 'myLib2', importPath: '@myorg/lib',
publishable: true, });
importPath: '@myorg/lib',
},
tree1
);
} catch (e) { } catch (e) {
expect(e.message).toContain( expect(e.message).toContain(
'You already have a library using the import path' 'You already have a library using the import path'
@ -437,22 +366,21 @@ describe('lib', () => {
describe(`--babelJest`, () => { describe(`--babelJest`, () => {
it('should use babel for jest', async () => { it('should use babel for jest', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ name: 'myLib', babelJest: true } as Schema, babelJest: true,
appTree } as Schema);
);
expect(tree.readContent(`libs/my-lib/jest.config.js`)) expect(tree.read(`libs/my-lib/jest.config.js`).toString())
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"module.exports = { "module.exports = {
displayName: 'my-lib', displayName: 'my-lib',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
transform: { transform: {
'^.+\\\\\\\\.[tj]sx?$': 'babel-jest', '^.+\\\\\\\\.[tj]sx?$': 'babel-jest'
}, },
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/my-lib', coverageDirectory: '../../coverage/libs/my-lib'
}; };
" "
`); `);
@ -460,14 +388,10 @@ describe('lib', () => {
}); });
describe('--js flag', () => { describe('--js flag', () => {
it('should generate js files instead of ts files', async () => { it('should generate js files instead of ts files', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ js: true,
name: 'myLib', } as Schema);
js: true,
} as Schema,
appTree
);
expect(tree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy(); expect(tree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy();
expect(tree.exists('libs/my-lib/src/index.js')).toBeTruthy(); expect(tree.exists('libs/my-lib/src/index.js')).toBeTruthy();
@ -475,37 +399,35 @@ describe('lib', () => {
expect(tree.exists('libs/my-lib/src/lib/my-lib.spec.js')).toBeTruthy(); expect(tree.exists('libs/my-lib/src/lib/my-lib.spec.js')).toBeTruthy();
expect( expect(
readJsonInTree(tree, 'libs/my-lib/tsconfig.json').compilerOptions readJson(tree, 'libs/my-lib/tsconfig.json').compilerOptions
).toEqual({ ).toEqual({
allowJs: true, allowJs: true,
}); });
expect( expect(readJson(tree, 'libs/my-lib/tsconfig.lib.json').include).toEqual([
readJsonInTree(tree, 'libs/my-lib/tsconfig.lib.json').include '**/*.ts',
).toEqual(['**/*.ts', '**/*.js']); '**/*.js',
expect( ]);
readJsonInTree(tree, 'libs/my-lib/tsconfig.lib.json').exclude expect(readJson(tree, 'libs/my-lib/tsconfig.lib.json').exclude).toEqual([
).toEqual(['**/*.spec.ts', '**/*.spec.js']); '**/*.spec.ts',
'**/*.spec.js',
]);
}); });
it('should update root tsconfig.json with a js file path', async () => { it('should update root tsconfig.json with a js file path', async () => {
const tree = await runSchematic( await libraryGenerator(tree, { name: 'myLib', js: true } as Schema);
'lib', const tsconfigJson = readJson(tree, '/tsconfig.base.json');
{ name: 'myLib', js: true } as Schema,
appTree
);
const tsconfigJson = readJsonInTree(tree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
'libs/my-lib/src/index.js', 'libs/my-lib/src/index.js',
]); ]);
}); });
it('should update architect builder when --buildable', async () => { it('should update architect builder when --buildable', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ name: 'myLib', buildable: true, js: true } as Schema, buildable: true,
appTree js: true,
); } as Schema);
const workspaceJson = readJsonInTree(tree, '/workspace.json'); const workspaceJson = readJson(tree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib'); expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
@ -515,11 +437,11 @@ describe('lib', () => {
}); });
it('should generate js files for nested libs as well', async () => { it('should generate js files for nested libs as well', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ name: 'myLib', directory: 'myDir', js: true } as Schema, directory: 'myDir',
appTree js: true,
); } as Schema);
expect(tree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy(); expect(tree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy();
expect(tree.exists('libs/my-dir/my-lib/src/index.js')).toBeTruthy(); expect(tree.exists('libs/my-dir/my-lib/src/index.js')).toBeTruthy();
expect( expect(
@ -533,21 +455,20 @@ describe('lib', () => {
describe('--pascalCaseFiles', () => { describe('--pascalCaseFiles', () => {
it('should generate files with upper case names', async () => { it('should generate files with upper case names', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ name: 'myLib', pascalCaseFiles: true } as Schema, pascalCaseFiles: true,
appTree } as Schema);
);
expect(tree.exists('libs/my-lib/src/lib/MyLib.ts')).toBeTruthy(); expect(tree.exists('libs/my-lib/src/lib/MyLib.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/lib/MyLib.spec.ts')).toBeTruthy(); expect(tree.exists('libs/my-lib/src/lib/MyLib.spec.ts')).toBeTruthy();
}); });
it('should generate files with upper case names for nested libs as well', async () => { it('should generate files with upper case names for nested libs as well', async () => {
const tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'myLib',
{ name: 'myLib', directory: 'myDir', pascalCaseFiles: true } as Schema, directory: 'myDir',
appTree pascalCaseFiles: true,
); } as Schema);
expect( expect(
tree.exists('libs/my-dir/my-lib/src/lib/MyDirMyLib.ts') tree.exists('libs/my-dir/my-lib/src/lib/MyDirMyLib.ts')
).toBeTruthy(); ).toBeTruthy();

View File

@ -0,0 +1,140 @@
import {
convertNxGenerator,
formatFiles,
generateFiles,
getWorkspaceLayout,
joinPathFragments,
names,
offsetFromRoot,
readProjectConfiguration,
toJS,
Tree,
updateProjectConfiguration,
updateTsConfigsToJs,
} from '@nrwl/devkit';
import { Schema } from './schema';
import { libraryGenerator as workspaceLibraryGenerator } from '@nrwl/workspace/generators';
import { join } from 'path';
export interface NormalizedSchema extends Schema {
name: string;
prefix: string;
fileName: string;
projectRoot: string;
projectDirectory: string;
parsedTags: string[];
}
export async function libraryGenerator(tree: Tree, schema: Schema) {
const options = normalizeOptions(tree, schema);
if (options.publishable === true && !schema.importPath) {
throw new Error(
`For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)`
);
}
const libraryInstall = await workspaceLibraryGenerator(tree, {
...schema,
importPath: options.importPath,
});
createFiles(tree, options);
if (options.js) {
updateTsConfigsToJs(tree, options);
}
updateProject(tree, options);
await formatFiles(tree);
return libraryInstall;
}
export default libraryGenerator;
export const librarySchematic = convertNxGenerator(libraryGenerator);
function normalizeOptions(tree: Tree, options: Schema): NormalizedSchema {
const { npmScope, libsDir } = getWorkspaceLayout(tree);
const defaultPrefix = npmScope;
const name = names(options.name).fileName;
const projectDirectory = options.directory
? `${names(options.directory).fileName}/${name}`
: name;
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
const fileName = projectName;
const projectRoot = joinPathFragments(libsDir, projectDirectory);
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
const importPath =
options.importPath || `@${defaultPrefix}/${projectDirectory}`;
return {
...options,
prefix: defaultPrefix, // we could also allow customizing this
fileName,
name: projectName,
projectRoot,
projectDirectory,
parsedTags,
importPath,
};
}
function createFiles(tree: Tree, options: NormalizedSchema) {
const nameFormats = names(options.name);
return generateFiles(
tree,
join(__dirname, './files/lib'),
options.projectRoot,
{
...options,
...nameFormats,
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
}
);
if (options.unitTestRunner === 'none') {
tree.delete(
join(options.projectRoot, `./src/lib/${nameFormats.fileName}.spec.ts`)
);
}
if (!options.publishable && !options.buildable) {
tree.delete(join(options.projectRoot, 'package.json'));
}
if (options.js) {
toJS(tree);
}
}
function updateProject(tree: Tree, options: NormalizedSchema) {
if (!options.publishable && !options.buildable) {
return;
}
const project = readProjectConfiguration(tree, options.name);
const { libsDir } = getWorkspaceLayout(tree);
project.targets = project.targets || {};
project.targets.build = {
executor: '@nrwl/node:package',
outputs: ['{options.outputPath}'],
options: {
outputPath: `dist/${libsDir}/${options.projectDirectory}`,
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
packageJson: `${options.projectRoot}/package.json`,
main: `${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'),
assets: [`${options.projectRoot}/*.md`],
},
};
if (options.rootDir) {
project.targets.build.options.srcRootForCompilationRoot = options.rootDir;
}
updateProjectConfiguration(tree, options.name, project);
}

View File

@ -3,18 +3,18 @@ import { Linter } from '@nrwl/workspace';
export interface Schema { export interface Schema {
name: string; name: string;
directory?: string; directory?: string;
skipTsConfig: boolean; skipTsConfig?: boolean;
skipFormat: boolean; skipFormat?: boolean;
tags?: string; tags?: string;
unitTestRunner: 'jest' | 'none'; unitTestRunner?: 'jest' | 'none';
linter: Linter; linter?: Linter;
buildable?: boolean; buildable?: boolean;
publishable?: boolean; publishable?: boolean;
importPath?: string; importPath?: string;
testEnvironment: 'jsdom' | 'node'; testEnvironment?: 'jsdom' | 'node';
rootDir?: string; rootDir?: string;
babelJest?: boolean; babelJest?: boolean;
js: boolean; js?: boolean;
pascalCaseFiles: boolean; pascalCaseFiles?: boolean;
strict: boolean; strict?: boolean;
} }

View File

@ -1,5 +1,6 @@
{ {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"cli": "nx",
"id": "NxNodeLibrary", "id": "NxNodeLibrary",
"title": "Create a Node Library for Nx", "title": "Create a Node Library for Nx",
"type": "object", "type": "object",

View File

@ -1,247 +0,0 @@
import {
apply,
chain,
externalSchematic,
mergeWith,
move,
noop,
Rule,
SchematicContext,
template,
Tree,
url,
} from '@angular-devkit/schematics';
import { join, normalize, Path } from '@angular-devkit/core';
import { Schema } from './schema';
import {
updateJsonInTree,
updateWorkspaceInTree,
generateProjectLint,
addLintFiles,
formatFiles,
} from '@nrwl/workspace';
import { getProjectConfig } from '@nrwl/workspace';
import init from '../init/init';
import { appsDir } from '@nrwl/workspace/src/utils/ast-utils';
import {
toJS,
updateTsConfigsToJs,
maybeJs,
} from '@nrwl/workspace/src/utils/rules/to-js';
import { names, offsetFromRoot } from '@nrwl/devkit';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
interface NormalizedSchema extends Schema {
appProjectRoot: Path;
parsedTags: string[];
}
function updateNxJson(options: NormalizedSchema): Rule {
return updateJsonInTree(`/nx.json`, (json) => {
return {
...json,
projects: {
...json.projects,
[options.name]: { tags: options.parsedTags },
},
};
});
}
function getBuildConfig(project: any, options: NormalizedSchema) {
return {
builder: '@nrwl/node:build',
outputs: ['{options.outputPath}'],
options: {
outputPath: join(normalize('dist'), options.appProjectRoot),
main: maybeJs(options, join(project.sourceRoot, 'main.ts')),
tsConfig: join(options.appProjectRoot, 'tsconfig.app.json'),
assets: [join(project.sourceRoot, 'assets')],
},
configurations: {
production: {
optimization: true,
extractLicenses: true,
inspect: false,
fileReplacements: [
{
replace: maybeJs(
options,
join(project.sourceRoot, 'environments/environment.ts')
),
with: maybeJs(
options,
join(project.sourceRoot, 'environments/environment.prod.ts')
),
},
],
},
},
};
}
function getServeConfig(options: NormalizedSchema) {
return {
builder: '@nrwl/node:execute',
options: {
buildTarget: `${options.name}:build`,
},
};
}
function updateWorkspaceJson(options: NormalizedSchema): Rule {
return updateWorkspaceInTree((workspaceJson) => {
const project = {
root: options.appProjectRoot,
sourceRoot: join(options.appProjectRoot, 'src'),
projectType: 'application',
prefix: options.name,
architect: <any>{},
};
project.architect.build = getBuildConfig(project, options);
project.architect.serve = getServeConfig(options);
project.architect.lint = generateProjectLint(
normalize(project.root),
join(normalize(project.root), 'tsconfig.app.json'),
options.linter,
[`${options.appProjectRoot}/**/*.${options.js ? 'js' : 'ts'}`]
);
workspaceJson.projects[options.name] = project;
workspaceJson.defaultProject = workspaceJson.defaultProject || options.name;
return workspaceJson;
});
}
function addAppFiles(options: NormalizedSchema): Rule {
return chain([
mergeWith(
apply(url(`./files/app`), [
template({
tmpl: '',
name: options.name,
root: options.appProjectRoot,
offset: offsetFromRoot(options.appProjectRoot),
}),
move(options.appProjectRoot),
options.js ? toJS() : noop(),
])
),
options.pascalCaseFiles
? (tree, context) => {
context.logger.warn('NOTE: --pascalCaseFiles is a noop');
return tree;
}
: noop(),
]);
}
function addProxy(options: NormalizedSchema): Rule {
return (host: Tree, context: SchematicContext) => {
const projectConfig = getProjectConfig(host, options.frontendProject);
if (projectConfig.architect && projectConfig.architect.serve) {
const pathToProxyFile = `${projectConfig.root}/proxy.conf.json`;
if (!host.exists(pathToProxyFile)) {
host.create(
pathToProxyFile,
JSON.stringify(
{
'/api': {
target: 'http://localhost:3333',
secure: false,
},
},
null,
2
)
);
} else {
//add new entry to existing config
const proxyFileContent = host.get(pathToProxyFile).content.toString();
const proxyModified = {
...JSON.parse(proxyFileContent),
[`/${options.name}-api`]: {
target: 'http://localhost:3333',
secure: false,
},
};
host.overwrite(pathToProxyFile, JSON.stringify(proxyModified, null, 2));
}
updateWorkspaceInTree((json) => {
projectConfig.architect.serve.options.proxyConfig = pathToProxyFile;
json.projects[options.frontendProject] = projectConfig;
return json;
})(host, context);
}
};
}
function addJest(options: NormalizedSchema) {
return options.unitTestRunner === 'jest'
? externalSchematic('@nrwl/jest', 'jest-project', {
project: options.name,
setupFile: 'none',
skipSerializers: true,
supportTsx: options.js,
babelJest: options.babelJest,
})
: noop();
}
export default function (schema: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(host, schema);
return chain([
init({
...options,
skipFormat: true,
}),
addLintFiles(options.appProjectRoot, options.linter),
addAppFiles(options),
options.js
? updateTsConfigsToJs({ projectRoot: options.appProjectRoot })
: noop,
updateWorkspaceJson(options),
updateNxJson(options),
addJest(options),
options.frontendProject ? addProxy(options) : noop(),
formatFiles(options),
])(host, context);
};
}
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
const appDirectory = options.directory
? `${names(options.directory).fileName}/${names(options.name).fileName}`
: names(options.name).fileName;
const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-');
const appProjectRoot = join(normalize(appsDir(host)), appDirectory);
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
return {
...options,
name: names(appProjectName).fileName,
frontendProject: options.frontendProject
? names(options.frontendProject).fileName
: undefined,
appProjectRoot,
parsedTags,
};
}
export const applicationGenerator = wrapAngularDevkitSchematic(
'@nrwl/node',
'application'
);

View File

@ -1,15 +0,0 @@
import { Linter } from '@nrwl/workspace';
export interface Schema {
name: string;
skipFormat: boolean;
skipPackageJson: boolean;
directory?: string;
unitTestRunner: 'jest' | 'none';
linter: Linter;
tags?: string;
frontendProject?: string;
babelJest?: boolean;
js: boolean;
pascalCaseFiles: boolean;
}

View File

@ -1,52 +0,0 @@
import { Tree } from '@angular-devkit/schematics';
import { addDepsToPackageJson, readJsonInTree } from '@nrwl/workspace';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule, runSchematic } from '../../utils/testing';
import { nxVersion } from '../../utils/versions';
describe('init', () => {
let tree: Tree;
beforeEach(() => {
tree = Tree.empty();
tree = createEmptyWorkspace(tree);
});
it('should add dependencies', async () => {
const existing = 'existing';
const existingVersion = '1.0.0';
await callRule(
addDepsToPackageJson(
{ '@nrwl/node': nxVersion, [existing]: existingVersion },
{ [existing]: existingVersion },
false
),
tree
);
const result = await runSchematic('init', {}, tree);
const packageJson = readJsonInTree(result, 'package.json');
expect(packageJson.dependencies['@nrwl/node']).toBeUndefined();
expect(packageJson.dependencies[existing]).toBeDefined();
expect(packageJson.devDependencies['@nrwl/node']).toBeDefined();
expect(packageJson.devDependencies[existing]).toBeDefined();
});
describe('defaultCollection', () => {
it('should be set if none was set before', async () => {
const result = await runSchematic('init', {}, tree);
const workspaceJson = readJsonInTree(result, 'workspace.json');
expect(workspaceJson.cli.defaultCollection).toEqual('@nrwl/node');
});
});
it('should not add jest config if unitTestRunner is none', async () => {
const result = await runSchematic(
'init',
{
unitTestRunner: 'none',
},
tree
);
expect(result.exists('jest.config.js')).toEqual(false);
});
});

View File

@ -1,28 +0,0 @@
import { chain, noop, Rule } from '@angular-devkit/schematics';
import {
addPackageWithInit,
formatFiles,
setDefaultCollection,
updateJsonInTree,
} from '@nrwl/workspace';
import { nxVersion } from '../../utils/versions';
import { Schema } from './schema';
function updateDependencies(): Rule {
return updateJsonInTree('package.json', (json) => {
delete json.dependencies['@nrwl/node'];
json.devDependencies['@nrwl/node'] = nxVersion;
return json;
});
}
export default function (schema: Schema) {
return chain([
setDefaultCollection('@nrwl/node'),
schema.unitTestRunner === 'jest'
? addPackageWithInit('@nrwl/jest')
: noop(),
updateDependencies(),
formatFiles(schema),
]);
}

View File

@ -1,4 +0,0 @@
export interface Schema {
unitTestRunner: 'jest' | 'none';
skipFormat: boolean;
}

View File

@ -1,148 +0,0 @@
import { normalize, Path } from '@angular-devkit/core';
import {
apply,
chain,
externalSchematic,
filter,
MergeStrategy,
mergeWith,
move,
noop,
Rule,
SchematicContext,
SchematicsException,
template,
Tree,
url,
} from '@angular-devkit/schematics';
import {
formatFiles,
getNpmScope,
updateWorkspaceInTree,
} from '@nrwl/workspace';
import { Schema } from './schema';
import { libsDir } from '@nrwl/workspace/src/utils/ast-utils';
import {
maybeJs,
toJS,
updateTsConfigsToJs,
} from '@nrwl/workspace/src/utils/rules/to-js';
import { names, offsetFromRoot } from '@nrwl/devkit';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
export interface NormalizedSchema extends Schema {
name: string;
prefix: string;
fileName: string;
projectRoot: Path;
projectDirectory: string;
parsedTags: string[];
}
export default function (schema: NormalizedSchema): Rule {
return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(host, schema);
if (options.publishable === true && !schema.importPath) {
throw new SchematicsException(
`For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)`
);
}
return chain([
externalSchematic('@nrwl/workspace', 'lib', {
...schema,
importPath: options.importPath,
}),
createFiles(options),
options.js ? updateTsConfigsToJs(options) : noop(),
addProject(options),
formatFiles(options),
]);
};
}
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
const defaultPrefix = getNpmScope(host);
const name = names(options.name).fileName;
const projectDirectory = options.directory
? `${names(options.directory).fileName}/${name}`
: name;
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
const fileName = projectName;
const projectRoot = normalize(`${libsDir(host)}/${projectDirectory}`);
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
const importPath =
options.importPath || `@${defaultPrefix}/${projectDirectory}`;
return {
...options,
prefix: defaultPrefix, // we could also allow customizing this
fileName,
name: projectName,
projectRoot,
projectDirectory,
parsedTags,
importPath,
};
}
function createFiles(options: NormalizedSchema): Rule {
return mergeWith(
apply(url(`./files/lib`), [
template({
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
}),
move(options.projectRoot),
options.unitTestRunner === 'none'
? filter((file) => !file.endsWith('spec.ts'))
: noop(),
options.publishable || options.buildable
? noop()
: filter((file) => !file.endsWith('package.json')),
options.js ? toJS() : noop(),
]),
MergeStrategy.Overwrite
);
}
function addProject(options: NormalizedSchema): Rule {
if (!options.publishable && !options.buildable) {
return noop();
}
return updateWorkspaceInTree((json, context, host) => {
const { architect } = json.projects[options.name];
if (architect) {
architect.build = {
builder: '@nrwl/node:package',
outputs: ['{options.outputPath}'],
options: {
outputPath: `dist/${libsDir(host)}/${options.projectDirectory}`,
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
packageJson: `${options.projectRoot}/package.json`,
main: maybeJs(options, `${options.projectRoot}/src/index.ts`),
assets: [`${options.projectRoot}/*.md`],
},
};
if (options.rootDir) {
architect.build.options.srcRootForCompilationRoot = options.rootDir;
}
}
return json;
});
}
export const libraryGenerator = wrapAngularDevkitSchematic(
'@nrwl/node',
'library'
);