feat: add --js and --pascalCaseFiles flags to core,node,express schematics (#3683)

This commit is contained in:
Martin Hochel 2020-11-17 18:29:20 +01:00 committed by GitHub
parent a1615e8346
commit 842455bbd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 829 additions and 135 deletions

View File

@ -21,6 +21,7 @@ module.exports = {
{ name: 'nextjs', description: 'anything Next specific' },
{ name: 'nest', description: 'anything Nest specific' },
{ name: 'node', description: 'anything Node specific' },
{ name: 'express', description: 'anything Express specific' },
{ name: 'nx-plugin', description: 'anything Nx Plugin specific' },
{ name: 'react', description: 'anything React specific' },
{ name: 'web', description: 'anything Web specific' },

View File

@ -48,6 +48,14 @@ Type: `string`
Frontend project that needs to access this application. This sets up proxy configuration.
### js
Default: `false`
Type: `boolean`
Generate JavaScript files rather than TypeScript files.
### linter
Default: `eslint`
@ -64,6 +72,16 @@ Type: `string`
The name of the application.
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### skipFormat
Default: `false`

View File

@ -48,6 +48,14 @@ Type: `string`
Frontend project that needs to access this application. This sets up proxy configuration.
### js
Default: `false`
Type: `boolean`
Generate JavaScript files rather than TypeScript files.
### linter
Default: `eslint`
@ -64,6 +72,16 @@ Type: `string`
The name of the application.
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### skipFormat
Default: `false`

View File

@ -66,6 +66,14 @@ Type: `string`
The library name used to import it, like @myorg/my-awesome-lib. Must be a valid npm name.
### js
Default: `false`
Type: `boolean`
Generate JavaScript files rather than TypeScript files.
### linter
Default: `eslint`
@ -82,6 +90,16 @@ Type: `string`
Library name
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### publishable
Type: `boolean`

View File

@ -80,6 +80,16 @@ Type: `string`
Library name
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### skipFormat
Default: `false`

View File

@ -48,6 +48,14 @@ Type: `string`
Frontend project that needs to access this application. This sets up proxy configuration.
### js
Default: `false`
Type: `boolean`
Generate JavaScript files rather than TypeScript files.
### linter
Default: `eslint`
@ -64,6 +72,16 @@ Type: `string`
The name of the application.
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### skipFormat
Default: `false`

View File

@ -48,6 +48,14 @@ Type: `string`
Frontend project that needs to access this application. This sets up proxy configuration.
### js
Default: `false`
Type: `boolean`
Generate JavaScript files rather than TypeScript files.
### linter
Default: `eslint`
@ -64,6 +72,16 @@ Type: `string`
The name of the application.
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### skipFormat
Default: `false`

View File

@ -66,6 +66,14 @@ Type: `string`
The library name used to import it, like @myorg/my-awesome-lib. Must be a valid npm name.
### js
Default: `false`
Type: `boolean`
Generate JavaScript files rather than TypeScript files.
### linter
Default: `eslint`
@ -82,6 +90,16 @@ Type: `string`
Library name
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### publishable
Type: `boolean`

View File

@ -80,6 +80,16 @@ Type: `string`
Library name
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### skipFormat
Default: `false`

View File

@ -48,6 +48,14 @@ Type: `string`
Frontend project that needs to access this application. This sets up proxy configuration.
### js
Default: `false`
Type: `boolean`
Generate JavaScript files rather than TypeScript files.
### linter
Default: `eslint`
@ -64,6 +72,16 @@ Type: `string`
The name of the application.
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### skipFormat
Default: `false`

View File

@ -48,6 +48,14 @@ Type: `string`
Frontend project that needs to access this application. This sets up proxy configuration.
### js
Default: `false`
Type: `boolean`
Generate JavaScript files rather than TypeScript files.
### linter
Default: `eslint`
@ -64,6 +72,16 @@ Type: `string`
The name of the application.
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### skipFormat
Default: `false`

View File

@ -66,6 +66,14 @@ Type: `string`
The library name used to import it, like @myorg/my-awesome-lib. Must be a valid npm name.
### js
Default: `false`
Type: `boolean`
Generate JavaScript files rather than TypeScript files.
### linter
Default: `eslint`
@ -82,6 +90,16 @@ Type: `string`
Library name
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### publishable
Type: `boolean`

View File

@ -80,6 +80,16 @@ Type: `string`
Library name
### pascalCaseFiles
Alias(es): P
Default: `false`
Type: `boolean`
Use pascal case file names.
### skipFormat
Default: `false`

View File

@ -60,9 +60,7 @@ forEachCli('nx', () => {
expect(workspaceJson).not.toContain('libs/');
const libTestResults = await runCLIAsync(`test ${expressLib}`);
expect(libTestResults.stdout).toContain(
'No tests found, exiting with code 0'
);
expect(libTestResults.stdout).toContain(`nx run ${expressLib}:test`);
const appBuildResults = await runCLIAsync(`build ${expressApp}`);
expect(appBuildResults.stdout).toContain(`nx run ${expressApp}:build`);

View File

@ -3,6 +3,8 @@ import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { runSchematic } from '../../utils/testing';
import { readJsonInTree } from '@nrwl/workspace';
import { Schema } from './schema.d';
describe('app', () => {
let appTree: Tree;
@ -12,26 +14,88 @@ describe('app', () => {
});
it('should generate files', async () => {
const tree = await runSchematic('app', { name: 'myNodeApp' }, appTree);
expect(tree.readContent('apps/my-node-app/src/main.ts')).toContain(
`import * as express from 'express';`
const tree = await runSchematic(
'app',
{ name: 'myNodeApp' } as Schema,
appTree
);
const mainFile = tree.readContent('apps/my-node-app/src/main.ts');
expect(mainFile).toContain(`import * as express from 'express';`);
const tsconfig = readJsonInTree(tree, 'apps/my-node-app/tsconfig.json');
expect(tsconfig).toMatchInlineSnapshot(`
Object {
"extends": "../../tsconfig.base.json",
"files": Array [],
"include": Array [],
"references": Array [
Object {
"path": "./tsconfig.app.json",
},
Object {
"path": "./tsconfig.spec.json",
},
],
}
`);
});
it('should add types to the tsconfig.app.json', async () => {
const tree = await runSchematic('app', { name: 'myNodeApp' }, appTree);
const tree = await runSchematic(
'app',
{ name: 'myNodeApp' } as Schema,
appTree
);
const tsconfig = readJsonInTree(tree, 'apps/my-node-app/tsconfig.app.json');
expect(tsconfig.compilerOptions.types).toContain('express');
expect(tsconfig).toMatchInlineSnapshot(`
Object {
"compilerOptions": Object {
"outDir": "../../dist/out-tsc",
"types": Array [
"node",
"express",
],
},
"exclude": Array [
"**/*.spec.ts",
],
"extends": "./tsconfig.json",
"include": Array [
"**/*.ts",
],
}
`);
});
it('should update tsconfig', async () => {
const tree = await runSchematic('app', { name: 'myNodeApp' }, appTree);
const tsconfig = readJsonInTree(tree, 'apps/my-node-app/tsconfig.json');
expect(tsconfig.references).toContainEqual({
path: './tsconfig.app.json',
describe('--js flag', () => {
it('should generate js files instead of ts files', async () => {
const tree = await runSchematic(
'app',
{
name: 'myNodeApp',
js: true,
} as Schema,
appTree
);
expect(tree.exists('apps/my-node-app/src/main.js')).toBeTruthy();
expect(tree.readContent('apps/my-node-app/src/main.js')).toContain(
`import * as express from 'express';`
);
const tsConfig = readJsonInTree(tree, 'apps/my-node-app/tsconfig.json');
expect(tsConfig.compilerOptions).toEqual({
allowJs: true,
});
expect(tsconfig.references).toContainEqual({
path: './tsconfig.spec.json',
const tsConfigApp = readJsonInTree(
tree,
'apps/my-node-app/tsconfig.app.json'
);
expect(tsConfigApp.include).toEqual(['**/*.ts', '**/*.js']);
expect(tsConfigApp.exclude).toEqual(['**/*.spec.ts', '**/*.spec.js']);
});
});
});

View File

@ -11,6 +11,7 @@ import { updateJsonInTree } from '@nrwl/workspace';
import { toFileName, formatFiles } from '@nrwl/workspace';
import init from '../init/init';
import { appsDir } from '@nrwl/workspace/src/utils/ast-utils';
import { maybeJs } from '@nrwl/workspace/src/utils/rules/to-js';
interface NormalizedSchema extends Schema {
appProjectRoot: Path;
@ -24,10 +25,10 @@ function addTypes(options: NormalizedSchema): Rule {
});
}
function addMainFile(options: NormalizedSchema): Rule {
function addAppFiles(options: NormalizedSchema): Rule {
return (host: Tree) => {
host.overwrite(
join(options.appProjectRoot, 'src/main.ts'),
maybeJs(options, join(options.appProjectRoot, 'src/main.ts')),
`/**
* This is not a production server yet!
* This is only a minimal backend to get started.
@ -57,7 +58,7 @@ export default function (schema: Schema): Rule {
return chain([
init({ ...options, skipFormat: true }),
externalSchematic('@nrwl/node', 'application', schema),
addMainFile(options),
addAppFiles(options),
addTypes(options),
formatFiles(options),
])(host, context);

View File

@ -11,4 +11,6 @@ export interface Schema {
linter: Linter;
frontendProject?: string;
babelJest?: boolean;
js: boolean;
pascalCaseFiles: boolean;
}

View File

@ -52,6 +52,17 @@
"type": "boolean",
"description": "Use babel instead ts-jest",
"default": false
},
"pascalCaseFiles": {
"type": "boolean",
"description": "Use pascal case file names.",
"alias": "P",
"default": false
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
}
},
"required": []

View File

@ -8,7 +8,7 @@ export default function updatePackageJson(
options: NormalizedBuilderOptions,
context: BuilderContext
) {
const mainFile = basename(options.main, '.ts');
const mainFile = basename(options.main).replace(/\.[tj]s$/, '');
const typingsFile = `${mainFile}.d.ts`;
const mainJsFile = `${mainFile}.js`;
const packageJson = readJsonFile(

View File

@ -1,6 +1,5 @@
import { Tree } from '@angular-devkit/schematics';
import * as stripJsonComments from 'strip-json-comments';
import { createEmptyWorkspace, getFileContent } from '@nrwl/workspace/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { runSchematic } from '../../utils/testing';
import { NxJson, readJsonInTree } from '@nrwl/workspace';
// to break the dependency
@ -24,7 +23,7 @@ describe('app', () => {
const project = workspaceJson.projects['my-node-app'];
expect(project.root).toEqual('apps/my-node-app');
expect(project.architect).toEqual(
jasmine.objectContaining({
expect.objectContaining({
build: {
builder: '@nrwl/node:build',
options: {
@ -85,37 +84,31 @@ describe('app', () => {
expect(tree.exists(`apps/my-node-app/jest.config.js`)).toBeTruthy();
expect(tree.exists('apps/my-node-app/src/main.ts')).toBeTruthy();
expect(tree.readContent('apps/my-node-app/tsconfig.json'))
.toMatchInlineSnapshot(`
"{
\\"extends\\": \\"../../tsconfig.base.json\\",
\\"files\\": [],
\\"include\\": [],
\\"references\\": [
{
\\"path\\": \\"./tsconfig.app.json\\"
const tsconfig = readJsonInTree(tree, 'apps/my-node-app/tsconfig.json');
expect(tsconfig).toMatchInlineSnapshot(`
Object {
"extends": "../../tsconfig.base.json",
"files": Array [],
"include": Array [],
"references": Array [
Object {
"path": "./tsconfig.app.json",
},
{
\\"path\\": \\"./tsconfig.spec.json\\"
Object {
"path": "./tsconfig.spec.json",
},
],
}
]
}
"
`);
const tsconfigApp = JSON.parse(
stripJsonComments(
getFileContent(tree, 'apps/my-node-app/tsconfig.app.json')
)
const tsconfigApp = readJsonInTree(
tree,
'apps/my-node-app/tsconfig.app.json'
);
expect(tsconfigApp.compilerOptions.outDir).toEqual('../../dist/out-tsc');
expect(tsconfigApp.extends).toEqual('./tsconfig.json');
const eslintrc = JSON.parse(
stripJsonComments(
getFileContent(tree, 'apps/my-node-app/.eslintrc.json')
)
);
const eslintrc = readJsonInTree(tree, 'apps/my-node-app/.eslintrc.json');
expect(eslintrc.extends).toEqual('../../.eslintrc.json');
});
});
@ -162,8 +155,7 @@ describe('app', () => {
it('should generate files', async () => {
const hasJsonValue = ({ path, expectedValue, lookupFn }) => {
const content = getFileContent(tree, path);
const config = JSON.parse(stripJsonComments(content));
const config = readJsonInTree(tree, path);
expect(lookupFn(config)).toEqual(expectedValue);
};
@ -243,7 +235,7 @@ describe('app', () => {
);
expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy();
const serve = JSON.parse(tree.readContent('workspace.json')).projects[
const serve = readJsonInTree(tree, 'workspace.json').projects[
'my-frontend'
].architect.serve;
expect(serve.options.proxyConfig).toEqual(
@ -261,7 +253,7 @@ describe('app', () => {
);
expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy();
const serve = JSON.parse(tree.readContent('workspace.json')).projects[
const serve = readJsonInTree(tree, 'workspace.json').projects[
'my-frontend'
].architect.serve;
expect(serve.options.proxyConfig).toEqual(
@ -313,4 +305,75 @@ describe('app', () => {
`);
});
});
describe('--js flag', () => {
it('should generate js files instead of ts files', async () => {
const tree = await runSchematic(
'app',
{
name: 'myNodeApp',
js: true,
} as Schema,
appTree
);
expect(tree.exists(`apps/my-node-app/jest.config.js`)).toBeTruthy();
expect(tree.exists('apps/my-node-app/src/main.js')).toBeTruthy();
const tsConfig = readJsonInTree(tree, 'apps/my-node-app/tsconfig.json');
expect(tsConfig.compilerOptions).toEqual({
allowJs: true,
});
const tsConfigApp = readJsonInTree(
tree,
'apps/my-node-app/tsconfig.app.json'
);
expect(tsConfigApp.include).toEqual(['**/*.ts', '**/*.js']);
expect(tsConfigApp.exclude).toEqual(['**/*.spec.ts', '**/*.spec.js']);
});
it('should update workspace.json', async () => {
const tree = await runSchematic(
'app',
{ name: 'myNodeApp', js: true } as Schema,
appTree
);
const workspaceJson = readJsonInTree(tree, '/workspace.json');
const project = workspaceJson.projects['my-node-app'];
const buildTarget = project.architect.build;
expect(buildTarget.options.main).toEqual('apps/my-node-app/src/main.js');
expect(buildTarget.configurations.production.fileReplacements).toEqual([
{
replace: 'apps/my-node-app/src/environments/environment.js',
with: 'apps/my-node-app/src/environments/environment.prod.js',
},
]);
});
it('should generate js files for nested libs as well', async () => {
const tree = await runSchematic(
'app',
{ name: 'myNodeApp', directory: 'myDir', js: true } as Schema,
appTree
);
expect(
tree.exists(`apps/my-dir/my-node-app/jest.config.js`)
).toBeTruthy();
expect(tree.exists('apps/my-dir/my-node-app/src/main.js')).toBeTruthy();
});
});
describe('--pascalCaseFiles', () => {
it(`should notify that this flag doesn't do anything`, async () => {
const tree = await runSchematic(
'app',
{ name: 'myNodeApp', pascalCaseFiles: true } as Schema,
appTree
);
// @TODO how to spy on context ?
// expect(contextLoggerSpy).toHaveBeenCalledWith('NOTE: --pascalCaseFiles is a noop')
});
});
});

View File

@ -25,6 +25,11 @@ import { getProjectConfig } from '@nrwl/workspace';
import { offsetFromRoot } 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';
interface NormalizedSchema extends Schema {
appProjectRoot: Path;
@ -48,7 +53,7 @@ function getBuildConfig(project: any, options: NormalizedSchema) {
builder: '@nrwl/node:build',
options: {
outputPath: join(normalize('dist'), options.appProjectRoot),
main: join(project.sourceRoot, 'main.ts'),
main: maybeJs(options, join(project.sourceRoot, 'main.ts')),
tsConfig: join(options.appProjectRoot, 'tsconfig.app.json'),
assets: [join(project.sourceRoot, 'assets')],
},
@ -59,8 +64,14 @@ function getBuildConfig(project: any, options: NormalizedSchema) {
inspect: false,
fileReplacements: [
{
replace: join(project.sourceRoot, 'environments/environment.ts'),
with: join(project.sourceRoot, 'environments/environment.prod.ts'),
replace: maybeJs(
options,
join(project.sourceRoot, 'environments/environment.ts')
),
with: maybeJs(
options,
join(project.sourceRoot, 'environments/environment.prod.ts')
),
},
],
},
@ -106,7 +117,8 @@ function updateWorkspaceJson(options: NormalizedSchema): Rule {
}
function addAppFiles(options: NormalizedSchema): Rule {
return mergeWith(
return chain([
mergeWith(
apply(url(`./files/app`), [
template({
tmpl: '',
@ -115,8 +127,16 @@ function addAppFiles(options: NormalizedSchema): Rule {
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 {
@ -147,6 +167,18 @@ function addProxy(options: NormalizedSchema): Rule {
};
}
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);
@ -157,16 +189,12 @@ export default function (schema: Schema): Rule {
}),
addLintFiles(options.appProjectRoot, options.linter),
addAppFiles(options),
options.js
? updateTsConfigsToJs({ projectRoot: options.appProjectRoot })
: noop,
updateWorkspaceJson(options),
updateNxJson(options),
options.unitTestRunner === 'jest'
? externalSchematic('@nrwl/jest', 'jest-project', {
project: options.name,
setupFile: 'none',
skipSerializers: true,
babelJest: options.babelJest,
})
: noop(),
addJest(options),
options.frontendProject ? addProxy(options) : noop(),
formatFiles(options),
])(host, context);

View File

@ -10,4 +10,6 @@ export interface Schema {
tags?: string;
frontendProject?: string;
babelJest?: boolean;
js: boolean;
pascalCaseFiles: boolean;
}

View File

@ -51,6 +51,17 @@
"type": "boolean",
"description": "Use babel instead ts-jest",
"default": false
},
"pascalCaseFiles": {
"type": "boolean",
"description": "Use pascal case file names.",
"alias": "P",
"default": false
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
}
},
"required": []

View File

@ -71,15 +71,21 @@ describe('lib', () => {
it('should create a local tsconfig.json', async () => {
const tree = await runSchematic('lib', { name: 'myLib' }, appTree);
const tsconfigJson = readJsonInTree(tree, 'libs/my-lib/tsconfig.json');
expect(tsconfigJson.extends).toEqual('../../tsconfig.base.json');
expect(tsconfigJson.references).toEqual([
{
path: './tsconfig.lib.json',
expect(tsconfigJson).toMatchInlineSnapshot(`
Object {
"extends": "../../tsconfig.base.json",
"files": Array [],
"include": Array [],
"references": Array [
Object {
"path": "./tsconfig.lib.json",
},
{
path: './tsconfig.spec.json',
Object {
"path": "./tsconfig.spec.json",
},
]);
],
}
`);
});
it('should extend the local tsconfig.json with tsconfig.spec.json', async () => {
@ -280,7 +286,21 @@ describe('lib', () => {
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
expect(workspaceJson.projects['my-lib'].architect.build).toBeDefined();
expect(workspaceJson.projects['my-lib'].architect.build)
.toMatchInlineSnapshot(`
Object {
"builder": "@nrwl/node:package",
"options": Object {
"assets": Array [
"libs/my-lib/*.md",
],
"main": "libs/my-lib/src/index.ts",
"outputPath": "dist/libs/my-lib",
"packageJson": "libs/my-lib/package.json",
"tsConfig": "libs/my-lib/tsconfig.lib.json",
},
}
`);
});
});
@ -413,4 +433,102 @@ describe('lib', () => {
`);
});
});
describe('--js flag', () => {
it('should generate js files instead of ts files', async () => {
const tree = await runSchematic(
'lib',
{
name: 'myLib',
js: true,
} as Schema,
appTree
);
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/lib/my-lib.js')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/lib/my-lib.spec.js')).toBeTruthy();
expect(
readJsonInTree(tree, 'libs/my-lib/tsconfig.json').compilerOptions
).toEqual({
allowJs: true,
});
expect(
readJsonInTree(tree, 'libs/my-lib/tsconfig.lib.json').include
).toEqual(['**/*.ts', '**/*.js']);
expect(
readJsonInTree(tree, 'libs/my-lib/tsconfig.lib.json').exclude
).toEqual(['**/*.spec.ts', '**/*.spec.js']);
});
it('should update root tsconfig.json with a js file path', async () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', js: true } as Schema,
appTree
);
const tsconfigJson = readJsonInTree(tree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
'libs/my-lib/src/index.js',
]);
});
it('should update architect builder when --buildable', async () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', buildable: true, js: true } as Schema,
appTree
);
const workspaceJson = readJsonInTree(tree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
expect(
workspaceJson.projects['my-lib'].architect.build.options.main
).toEqual('libs/my-lib/src/index.js');
});
it('should generate js files for nested libs as well', async () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', directory: 'myDir', js: true } as Schema,
appTree
);
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/lib/my-dir-my-lib.js')
).toBeTruthy();
expect(
tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.spec.js')
).toBeTruthy();
});
});
describe('--pascalCaseFiles', () => {
it('should generate files with upper case names', async () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', pascalCaseFiles: true } as Schema,
appTree
);
expect(tree.exists('libs/my-lib/src/lib/MyLib.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 () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', directory: 'myDir', pascalCaseFiles: true } as Schema,
appTree
);
expect(
tree.exists('libs/my-dir/my-lib/src/lib/MyDirMyLib.ts')
).toBeTruthy();
expect(
tree.exists('libs/my-dir/my-lib/src/lib/MyDirMyLib.spec.ts')
).toBeTruthy();
});
});
});

View File

@ -25,6 +25,11 @@ import {
} from '@nrwl/workspace';
import { Schema } from './schema';
import { libsDir } from '@nrwl/workspace/src/utils/ast-utils';
import {
toJS,
updateTsConfigsToJs,
maybeJs,
} from '@nrwl/workspace/src/utils/rules/to-js';
export interface NormalizedSchema extends Schema {
name: string;
@ -51,6 +56,7 @@ export default function (schema: NormalizedSchema): Rule {
importPath: options.importPath,
}),
createFiles(options),
options.js ? updateTsConfigsToJs(options) : noop(),
addProject(options),
formatFiles(options),
]);
@ -103,6 +109,7 @@ function createFiles(options: NormalizedSchema): Rule {
options.publishable || options.buildable
? noop()
: filter((file) => !file.endsWith('package.json')),
options.js ? toJS() : noop(),
]),
MergeStrategy.Overwrite
);
@ -122,7 +129,7 @@ function addProject(options: NormalizedSchema): Rule {
outputPath: `dist/${libsDir(host)}/${options.projectDirectory}`,
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
packageJson: `${options.projectRoot}/package.json`,
main: `${options.projectRoot}/src/index.ts`,
main: maybeJs(options, `${options.projectRoot}/src/index.ts`),
assets: [`${options.projectRoot}/*.md`],
},
};

View File

@ -14,4 +14,6 @@ export interface Schema {
testEnvironment: 'jsdom' | 'node';
rootDir?: string;
babelJest?: boolean;
js: boolean;
pascalCaseFiles: boolean;
}

View File

@ -79,6 +79,17 @@
"type": "boolean",
"description": "Use babel instead ts-jest",
"default": false
},
"pascalCaseFiles": {
"type": "boolean",
"description": "Use pascal case file names.",
"alias": "P",
"default": false
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
}
},
"required": ["name"]

View File

@ -0,0 +1,7 @@
import { <%= propertyName %> } from './<%= fileName %>';
describe('<%= propertyName %>', () => {
it('should work', () => {
expect(<%= propertyName %>()).toEqual('<%= name %>');
})
})

View File

@ -0,0 +1,3 @@
export function <%= propertyName %>(): string {
return '<%= name %>';
}

View File

@ -70,14 +70,21 @@ describe('lib', () => {
it('should create a local tsconfig.json', async () => {
const tree = await runSchematic('lib', { name: 'myLib' }, appTree);
const tsconfigJson = readJsonInTree(tree, 'libs/my-lib/tsconfig.json');
expect(tsconfigJson.references).toEqual([
{
path: './tsconfig.lib.json',
expect(tsconfigJson).toMatchInlineSnapshot(`
Object {
"extends": "../../tsconfig.base.json",
"files": Array [],
"include": Array [],
"references": Array [
Object {
"path": "./tsconfig.lib.json",
},
{
path: './tsconfig.spec.json',
Object {
"path": "./tsconfig.spec.json",
},
]);
],
}
`);
});
it('should extend the local tsconfig.json with tsconfig.spec.json', async () => {
@ -126,6 +133,7 @@ describe('lib', () => {
`);
expect(tree.exists('libs/my-lib/src/index.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/lib/my-lib.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/lib/my-lib.spec.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/README.md')).toBeTruthy();
const ReadmeContent = tree.readContent('libs/my-lib/README.md');
@ -183,6 +191,9 @@ describe('lib', () => {
expect(
tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.ts')
).toBeTruthy();
expect(
tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.spec.ts')
).toBeTruthy();
expect(tree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy();
expect(tree.exists(`libs/my-dir/my-lib/.eslintrc.json`)).toBeTruthy();
});
@ -255,14 +266,19 @@ describe('lib', () => {
});
describe('--unit-test-runner none', () => {
it('should not generate test configuration', async () => {
it('should not generate test configuration nor spec file', async () => {
const resultTree = await runSchematic(
'lib',
{ name: 'myLib', unitTestRunner: 'none' },
appTree
);
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/src/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.lint)
@ -334,6 +350,31 @@ describe('lib', () => {
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/lib/my-lib.js')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/lib/my-lib.spec.js')).toBeTruthy();
});
it('should update tsconfig.json with compilerOptions.allowJs: true', async () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', js: true },
appTree
);
expect(
readJsonInTree(tree, 'libs/my-lib/tsconfig.json').compilerOptions
).toEqual({
allowJs: true,
});
});
it('should update tsconfig.lib.json include with **/*.js glob', async () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', js: true },
appTree
);
expect(
readJsonInTree(tree, 'libs/my-lib/tsconfig.lib.json').include
).toEqual(['**/*.ts', '**/*.js']);
});
it('should update root tsconfig.json with a js file path', async () => {
@ -359,6 +400,9 @@ describe('lib', () => {
expect(
tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.js')
).toBeTruthy();
expect(
tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.spec.js')
).toBeTruthy();
expect(tree.exists('libs/my-dir/my-lib/src/index.js')).toBeTruthy();
});
});
@ -407,4 +451,29 @@ describe('lib', () => {
`);
});
});
describe('--pascalCaseFiles', () => {
it('should generate files with upper case names', async () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', pascalCaseFiles: true },
appTree
);
expect(tree.exists('libs/my-lib/src/lib/MyLib.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 () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', directory: 'myDir', pascalCaseFiles: true },
appTree
);
expect(
tree.exists('libs/my-dir/my-lib/src/lib/MyDirMyLib.ts')
).toBeTruthy();
expect(
tree.exists('libs/my-dir/my-lib/src/lib/MyDirMyLib.spec.ts')
).toBeTruthy();
});
});
});

View File

@ -11,20 +11,25 @@ import {
move,
noop,
SchematicsException,
filter,
} from '@angular-devkit/schematics';
import { join, normalize } from '@angular-devkit/core';
import { Schema } from './schema';
import { updateWorkspaceInTree, getNpmScope } from '@nrwl/workspace';
import { updateJsonInTree } from '@nrwl/workspace';
import { toFileName, names } from '@nrwl/workspace';
import { formatFiles } from '@nrwl/workspace';
import { offsetFromRoot } from '@nrwl/workspace';
import {
updateWorkspaceInTree,
getNpmScope,
toFileName,
names,
updateJsonInTree,
formatFiles,
offsetFromRoot,
} from '@nrwl/workspace';
import { generateProjectLint, addLintFiles } from '../../utils/lint';
import { addProjectToNxJsonInTree, libsDir } from '../../utils/ast-utils';
import { cliCommand } from '../../core/file-utils';
import { toJS } from '../../utils/rules/to-js';
import { toJS, updateTsConfigsToJs, maybeJs } from '../../utils/rules/to-js';
export interface NormalizedSchema extends Schema {
name: string;
@ -82,17 +87,22 @@ function updateTsConfig(options: NormalizedSchema): Rule {
}
function createFiles(options: NormalizedSchema): Rule {
const { className, name, propertyName } = names(options.name);
return mergeWith(
apply(url(`./files/lib`), [
template({
...options,
...names(options.name),
className,
name,
propertyName,
cliCommand: cliCommand(),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
hasUnitTestRunner: options.unitTestRunner !== 'none',
}),
move(options.projectRoot),
addTestFiles(options),
options.js ? toJS() : noop(),
])
);
@ -102,17 +112,8 @@ function updateNxJson(options: NormalizedSchema): Rule {
return addProjectToNxJsonInTree(options.name, { tags: options.parsedTags });
}
export default function (schema: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(host, schema);
return chain([
addLintFiles(options.projectRoot, options.linter),
createFiles(options),
!options.skipTsConfig ? updateTsConfig(options) : noop(),
addProject(options),
updateNxJson(options),
options.unitTestRunner !== 'none'
function addJest(options: NormalizedSchema) {
return options.unitTestRunner !== 'none'
? externalSchematic('@nrwl/jest', 'jest-project', {
project: options.name,
setupFile: 'none',
@ -121,7 +122,21 @@ export default function (schema: Schema): Rule {
skipSerializers: true,
testEnvironment: options.testEnvironment,
})
: noop(),
: noop();
}
export default function (schema: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(host, schema);
return chain([
addLintFiles(options.projectRoot, options.linter),
createFiles(options),
options.js ? updateTsConfigsToJs(options) : noop(),
!options.skipTsConfig ? updateTsConfig(options) : noop(),
addProject(options),
updateNxJson(options),
addJest(options),
formatFiles(options),
])(host, context);
};
@ -134,9 +149,11 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
: name;
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
const fileName = options.simpleModuleName ? name : projectName;
const fileName = getCaseAwareFileName({
fileName: options.simpleModuleName ? name : projectName,
pascalCaseFiles: options.pascalCaseFiles,
});
// const projectRoot = `libs/${projectDirectory}`;
const projectRoot = `${libsDir(host)}/${projectDirectory}`;
const parsedTags = options.tags
@ -157,8 +174,17 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
};
}
function maybeJs(options: NormalizedSchema, path: string): string {
return options.js && (path.endsWith('.ts') || path.endsWith('.tsx'))
? path.replace(/\.tsx?$/, '.js')
: path;
function getCaseAwareFileName(options: {
pascalCaseFiles: boolean;
fileName: string;
}) {
const normalized = names(options.fileName);
return options.pascalCaseFiles ? normalized.className : normalized.fileName;
}
function addTestFiles(options: Pick<Schema, 'unitTestRunner'>) {
return options.unitTestRunner === 'none'
? filter((path) => !(path.endsWith('.ts') || path.endsWith('.tsx')))
: noop();
}

View File

@ -11,6 +11,7 @@ export interface Schema {
linter: Linter;
testEnvironment: 'jsdom' | 'node';
importPath?: string;
js?: boolean;
js: boolean;
babelJest?: boolean;
pascalCaseFiles: boolean;
}

View File

@ -47,8 +47,8 @@
},
"skipTsConfig": {
"type": "boolean",
"default": false,
"description": "Do not update tsconfig.json for development experience."
"description": "Do not update tsconfig.json for development experience.",
"default": false
},
"testEnvironment": {
"type": "string",
@ -65,6 +65,12 @@
"description": "Use babel instead ts-jest",
"default": false
},
"pascalCaseFiles": {
"type": "boolean",
"description": "Use pascal case file names.",
"alias": "P",
"default": false
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files",

View File

@ -1,9 +1,18 @@
import { transpile, JsxEmit, ScriptTarget } from 'typescript';
import { forEach, Rule, when } from '@angular-devkit/schematics';
import {
forEach,
Rule,
when,
chain,
Tree,
SchematicsException,
} from '@angular-devkit/schematics';
import { normalize } from '@angular-devkit/core';
import { updateJsonInTree } from '../ast-utils';
export function toJS(): Rule {
return forEach(
return chain([
forEach(
when(
(path) => path.endsWith('.ts') || path.endsWith('.tsx'),
(entry) => {
@ -19,5 +28,67 @@ export function toJS(): Rule {
};
}
)
);
),
]);
}
export function updateTsConfigsToJs(options: { projectRoot: string }): Rule {
const paths = {
tsConfig: normalize(`${options.projectRoot}/tsconfig.json`),
tsConfigLib: normalize(`${options.projectRoot}/tsconfig.lib.json`),
tsConfigApp: normalize(`${options.projectRoot}/tsconfig.app.json`),
};
const getProjectType = (tree: Tree) => {
if (tree.exists(paths.tsConfigApp)) {
return 'application';
}
if (tree.exists(paths.tsConfigLib)) {
return 'library';
}
throw new SchematicsException(
`project is missing tsconfig.lib.json or tsconfig.app.json`
);
};
const getConfigFileForUpdate = (tree: Tree) => {
const projectType = getProjectType(tree);
if (projectType === 'library') {
return paths.tsConfigLib;
}
if (projectType === 'application') {
return paths.tsConfigApp;
}
};
return chain([
updateJsonInTree(paths.tsConfig, (json) => {
if (json.compilerOptions) {
json.compilerOptions.allowJs = true;
} else {
json.compilerOptions = { allowJs: true };
}
return json;
}),
(tree) => {
const updateConfigPath = getConfigFileForUpdate(tree);
return updateJsonInTree(updateConfigPath, (json) => {
json.include = uniq([...json.include, '**/*.js']);
json.exclude = uniq([...json.exclude, '**/*.spec.js']);
return json;
});
},
]);
}
const uniq = <T extends string[]>(value: T) => [...new Set(value)] as T;
export function maybeJs(options: { js: boolean }, path: string): string {
return options.js && (path.endsWith('.ts') || path.endsWith('.tsx'))
? path.replace(/\.tsx?$/, '.js')
: path;
}