diff --git a/docs/generated/api-angular/generators/component.md b/docs/generated/api-angular/generators/component.md new file mode 100644 index 0000000000000..3623ed472b62a --- /dev/null +++ b/docs/generated/api-angular/generators/component.md @@ -0,0 +1,156 @@ +--- +title: '@nrwl/angular:component generator' +description: 'Generate an Angular Component.' +--- + +# @nrwl/angular:component + +Generate an Angular Component. + +## Usage + +```bash +nx generate component ... +``` + +By default, Nx will search for `component` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/angular:component ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g component ... --dry-run +``` + +## Options + +### name (_**required**_) + +Type: `string` + +The name of the component. + +### changeDetection + +Alias(es): c + +Default: `Default` + +Type: `string` + +Possible values: `Default`, `OnPush` + +The change detection strategy to use in the new component. + +### displayBlock + +Alias(es): b + +Default: `false` + +Type: `boolean` + +Specifies if the style will contain `:host { display: block; }`. + +### export + +Default: `false` + +Type: `boolean` + +Specifies if the component should be exported in the declaring NgModule. Additionally, if the project is a library, the component will be exported from the project's entry point (normally `index.ts`). + +### flat + +Default: `false` + +Type: `boolean` + +Create the new files at the top level of the current project. + +### inlineStyle + +Alias(es): s + +Default: `false` + +Type: `boolean` + +Include styles inline in the component.ts file. Only CSS styles can be included inline. By default, an external styles file is created and referenced in the component.ts file. + +### inlineTemplate + +Alias(es): t + +Default: `false` + +Type: `boolean` + +Include template inline in the component.ts file. By default, an external template file is created and referenced in the component.ts file. + +### path (**hidden**) + +Type: `string` + +The path at which to create the component file, relative to the current workspace. Default is a folder with the same name as the component in the project root. + +### project + +Type: `string` + +The name of the project. + +### selector + +Type: `string` + +The HTML selector to use for this component. + +### skipSelector + +Default: `false` + +Type: `boolean` + +Specifies if the component should have a selector or not. + +### skipTests + +Default: `false` + +Type: `boolean` + +Do not create "spec.ts" test files for the new component. + +### style + +Default: `css` + +Type: `string` + +Possible values: `css`, `scss`, `sass`, `less`, `none` + +The file extension or preprocessor to use for style files, or 'none' to skip generating the style file. + +### type + +Default: `component` + +Type: `string` + +Adds a developer-defined type to the filename, in the format "name.type.ts". + +### viewEncapsulation + +Alias(es): v + +Type: `string` + +Possible values: `Emulated`, `None`, `ShadowDom` + +The view encapsulation strategy to use in the new component. diff --git a/docs/map.json b/docs/map.json index 0e893fde01e6a..d1e32f0ee45a1 100644 --- a/docs/map.json +++ b/docs/map.json @@ -644,6 +644,11 @@ "id": "application", "file": "generated/api-angular/generators/application" }, + { + "name": "component generator", + "id": "component", + "file": "generated/api-angular/generators/component" + }, { "name": "convert-tslint-to-eslint", "id": "convert-tslint-to-eslint", diff --git a/packages/angular/generators.json b/packages/angular/generators.json index 57df82b7a1a36..af1c6522d94be 100644 --- a/packages/angular/generators.json +++ b/packages/angular/generators.json @@ -16,6 +16,11 @@ "x-type": "application", "description": "Creates an Angular application." }, + "component": { + "factory": "./src/generators/component/component.compat", + "schema": "./src/generators/component/schema.json", + "description": "Generate an Angular Component." + }, "component-cypress-spec": { "factory": "./src/generators/component-cypress-spec/compat", "schema": "./src/generators/component-cypress-spec/schema.json", @@ -161,6 +166,11 @@ "x-type": "application", "description": "Creates an Angular application." }, + "component": { + "factory": "./src/generators/component/component", + "schema": "./src/generators/component/schema.json", + "description": "Generate an Angular Component." + }, "component-cypress-spec": { "factory": "./src/generators/component-cypress-spec/component-cypress-spec", "schema": "./src/generators/component-cypress-spec/schema.json", diff --git a/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap b/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap new file mode 100644 index 0000000000000..c52f65f44851f --- /dev/null +++ b/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`component Generator --flat should create the component correctly and export it 1`] = ` +"import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.css'] +}) +export class ExampleComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} +" +`; + +exports[`component Generator --flat should create the component correctly and not export it 1`] = ` +"import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.css'] +}) +export class ExampleComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} +" +`; + +exports[`component Generator --path should create the component correctly and export it 1`] = ` +"import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.css'] +}) +export class ExampleComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} +" +`; + +exports[`component Generator should create the component correctly and export it 1`] = ` +"import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.css'] +}) +export class ExampleComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} +" +`; + +exports[`component Generator should create the component correctly and export it 2`] = ` +" + export * from \\"./lib/example/example.component\\";" +`; + +exports[`component Generator should create the component correctly and not export it 1`] = ` +"import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.css'] +}) +export class ExampleComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} +" +`; + +exports[`component Generator should create the component correctly but not export it when no entry point exists 1`] = ` +"import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.css'] +}) +export class ExampleComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} +" +`; diff --git a/packages/angular/src/generators/component/component.compat.ts b/packages/angular/src/generators/component/component.compat.ts new file mode 100644 index 0000000000000..f73c31c33528b --- /dev/null +++ b/packages/angular/src/generators/component/component.compat.ts @@ -0,0 +1,4 @@ +import componentGenerator from './component'; +import { convertNxGenerator } from '@nrwl/devkit'; + +export default convertNxGenerator(componentGenerator); diff --git a/packages/angular/src/generators/component/component.spec.ts b/packages/angular/src/generators/component/component.spec.ts new file mode 100644 index 0000000000000..b5000272aaa11 --- /dev/null +++ b/packages/angular/src/generators/component/component.spec.ts @@ -0,0 +1,206 @@ +import { addProjectConfiguration } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import componentGenerator from './component'; + +describe('component Generator', () => { + it('should create the component correctly and export it', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'lib1', { + projectType: 'library', + sourceRoot: 'libs/lib1/src', + root: 'libs/lib1', + }); + tree.write('libs/lib1/src/index.ts', ''); + + // ACT + await componentGenerator(tree, { + name: 'example', + project: 'lib1', + export: true, + }); + + // ASSERT + const componentSource = tree.read( + 'libs/lib1/src/lib/example/example.component.ts', + 'utf-8' + ); + expect(componentSource).toMatchSnapshot(); + + const indexSource = tree.read('libs/lib1/src/index.ts', 'utf-8'); + expect(indexSource).toMatchSnapshot(); + }); + + it('should create the component correctly and not export it', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'lib1', { + projectType: 'library', + sourceRoot: 'libs/lib1/src', + root: 'libs/lib1', + }); + tree.write('libs/lib1/src/index.ts', ''); + + // ACT + await componentGenerator(tree, { + name: 'example', + project: 'lib1', + export: false, + }); + + // ASSERT + const componentSource = tree.read( + 'libs/lib1/src/lib/example/example.component.ts', + 'utf-8' + ); + expect(componentSource).toMatchSnapshot(); + + const indexSource = tree.read('libs/lib1/src/index.ts', 'utf-8'); + expect(indexSource).not.toContain( + `export * from "./lib/example/example.component"` + ); + }); + + it('should create the component correctly but not export it when no entry point exists', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'lib1', { + projectType: 'library', + sourceRoot: 'libs/lib1/src', + root: 'libs/lib1', + }); + + // ACT + await componentGenerator(tree, { + name: 'example', + project: 'lib1', + export: true, + }); + + // ASSERT + const componentSource = tree.read( + 'libs/lib1/src/lib/example/example.component.ts', + 'utf-8' + ); + expect(componentSource).toMatchSnapshot(); + + const indexExists = tree.exists('libs/lib1/src/index.ts'); + expect(indexExists).toBeFalsy(); + }); + + describe('--flat', () => { + it('should create the component correctly and export it', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'lib1', { + projectType: 'library', + sourceRoot: 'libs/lib1/src', + root: 'libs/lib1', + }); + tree.write('libs/lib1/src/index.ts', ''); + + // ACT + await componentGenerator(tree, { + name: 'example', + project: 'lib1', + flat: true, + export: true, + }); + + // ASSERT + const componentSource = tree.read( + 'libs/lib1/src/lib/example.component.ts', + 'utf-8' + ); + expect(componentSource).toMatchSnapshot(); + + const indexSource = tree.read('libs/lib1/src/index.ts', 'utf-8'); + expect(indexSource).toContain(`export * from "./lib/example.component"`); + }); + + it('should create the component correctly and not export it', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'lib1', { + projectType: 'library', + sourceRoot: 'libs/lib1/src', + root: 'libs/lib1', + }); + tree.write('libs/lib1/src/index.ts', ''); + + // ACT + await componentGenerator(tree, { + name: 'example', + project: 'lib1', + flat: true, + export: false, + }); + + // ASSERT + const componentSource = tree.read( + 'libs/lib1/src/lib/example.component.ts', + 'utf-8' + ); + expect(componentSource).toMatchSnapshot(); + + const indexSource = tree.read('libs/lib1/src/index.ts', 'utf-8'); + expect(indexSource).not.toContain( + `export * from "./lib/example.component"` + ); + }); + }); + + describe('--path', () => { + it('should create the component correctly and export it', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'lib1', { + projectType: 'library', + sourceRoot: 'libs/lib1/src', + root: 'libs/lib1', + }); + tree.write('libs/lib1/src/index.ts', ''); + + // ACT + await componentGenerator(tree, { + name: 'example', + project: 'lib1', + path: 'libs/lib1/src/lib/mycomp', + export: true, + }); + + // ASSERT + const componentSource = tree.read( + 'libs/lib1/src/lib/mycomp/example/example.component.ts', + 'utf-8' + ); + expect(componentSource).toMatchSnapshot(); + + const indexSource = tree.read('libs/lib1/src/index.ts', 'utf-8'); + expect(indexSource).toContain( + `export * from "./lib/mycomp/example/example.component"` + ); + }); + + it('should throw if the path specified is not under the project root', async () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(2); + addProjectConfiguration(tree, 'lib1', { + projectType: 'library', + sourceRoot: 'libs/lib1/src', + root: 'libs/lib1', + }); + tree.write('libs/lib1/src/index.ts', ''); + + // ACT & ASSERT + await expect( + componentGenerator(tree, { + name: 'example', + project: 'lib1', + path: 'apps/app1/src/mycomp', + export: false, + }) + ).rejects.toThrow(); + }); + }); +}); diff --git a/packages/angular/src/generators/component/component.ts b/packages/angular/src/generators/component/component.ts new file mode 100644 index 0000000000000..6617ff3b42aab --- /dev/null +++ b/packages/angular/src/generators/component/component.ts @@ -0,0 +1,123 @@ +import { joinPathFragments, logger, names, readJson, Tree } from '@nrwl/devkit'; +import type { Schema } from './schema'; +import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter'; +import { + formatFiles, + readWorkspaceConfiguration, + readProjectConfiguration, + normalizePath, +} from '@nrwl/devkit'; +import { normalize } from 'path'; +import { pathStartsWith } from '../utils/path'; + +export async function componentGenerator(tree: Tree, schema: Schema) { + checkPathUnderProjectRoot(tree, schema); + + const angularComponentSchematic = wrapAngularDevkitSchematic( + '@schematics/angular', + 'component' + ); + await angularComponentSchematic(tree, { + ...schema, + skipImport: true, + }); + + exportComponent(tree, schema); + + await formatFiles(tree); +} + +function checkPathUnderProjectRoot(tree: Tree, schema: Partial) { + if (!schema.path) { + return; + } + + const project = + schema.project ?? readWorkspaceConfiguration(tree).defaultProject; + const { root } = readProjectConfiguration(tree, project); + + let pathToComponent = normalizePath(schema.path); + pathToComponent = pathToComponent.startsWith('/') + ? pathToComponent.slice(1) + : pathToComponent; + + if (!pathStartsWith(pathToComponent, root)) { + throw new Error( + `The path provided for the component (${schema.path}) does not exist under the project root (${root}).` + ); + } +} + +function exportComponent(tree: Tree, schema: Schema) { + if (!schema.export) { + return; + } + + const project = + schema.project ?? readWorkspaceConfiguration(tree).defaultProject; + + const { root, sourceRoot, projectType } = readProjectConfiguration( + tree, + project + ); + + if (projectType === 'application') { + logger.warn( + '--export=true was ignored as the project the component being generated in is not a library.' + ); + + return; + } + + const componentNames = names(schema.name); + + const componentFileName = `${componentNames.fileName}.${ + schema.type ?? 'component' + }`; + + let componentDirectory = schema.flat + ? joinPathFragments(sourceRoot, 'lib') + : joinPathFragments(sourceRoot, 'lib', componentNames.fileName); + + if (schema.path) { + componentDirectory = schema.flat + ? normalizePath(schema.path) + : joinPathFragments(schema.path, componentNames.fileName); + } + + const componentFilePath = joinPathFragments( + componentDirectory, + `${componentFileName}.ts` + ); + + const ngPackageJsonPath = joinPathFragments(root, 'ng-package.json'); + const ngPackageEntryPoint = tree.exists(ngPackageJsonPath) + ? readJson(tree, ngPackageJsonPath).lib?.entryFile + : undefined; + + const projectEntryPoint = ngPackageEntryPoint + ? joinPathFragments(root, ngPackageEntryPoint) + : joinPathFragments(sourceRoot, `index.ts`); + + if (!tree.exists(projectEntryPoint)) { + // Let's not error, simply warn the user + // It's not too much effort to manually do this + // It would be more frustrating to have to find the correct path and re-run the command + logger.warn( + `Could not export the component in the library's entry point. Unable to determine project's entry point. The path ${projectEntryPoint} does not exist. The component has still been created.` + ); + + return; + } + + const relativePathFromEntryPoint = `.${componentFilePath + .split(sourceRoot)[1] + .replace('.ts', '')}`; + + const updateEntryPointContent = `${tree.read(projectEntryPoint, 'utf-8')} + export * from "${relativePathFromEntryPoint}";`; + + tree.write(projectEntryPoint, updateEntryPointContent); +} + +export default componentGenerator; diff --git a/packages/angular/src/generators/component/schema.d.ts b/packages/angular/src/generators/component/schema.d.ts new file mode 100644 index 0000000000000..621550ca44e9c --- /dev/null +++ b/packages/angular/src/generators/component/schema.d.ts @@ -0,0 +1,17 @@ +export interface Schema { + name: string; + path?: string; + project?: string; + displayBlock?: boolean; + inlineStyle?: boolean; + inlineTemplate?: boolean; + viewEncapsulation?: 'Emulated' | 'None' | 'ShadowDom'; + changeDetection?: 'Default' | 'OnPush'; + style?: 'css' | 'scss' | 'sass' | 'less' | 'none'; + skipTests?: boolean; + type?: string; + flat?: boolean; + selector?: string; + skipSelector?: boolean; + export?: boolean; +} diff --git a/packages/angular/src/generators/component/schema.json b/packages/angular/src/generators/component/schema.json new file mode 100644 index 0000000000000..63efc6915256a --- /dev/null +++ b/packages/angular/src/generators/component/schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "SchematicsAngularComponent", + "title": "Angular Component Schema", + "cli": "nx", + "type": "object", + "description": "Creates a new, generic component definition in the given or default project.", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "format": "path", + "description": "The path at which to create the component file, relative to the current workspace. Default is a folder with the same name as the component in the project root.", + "visible": false + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + } + }, + "name": { + "type": "string", + "description": "The name of the component.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the component?" + }, + "displayBlock": { + "description": "Specifies if the style will contain `:host { display: block; }`.", + "type": "boolean", + "default": false, + "alias": "b" + }, + "inlineStyle": { + "description": "Include styles inline in the component.ts file. Only CSS styles can be included inline. By default, an external styles file is created and referenced in the component.ts file.", + "type": "boolean", + "default": false, + "alias": "s" + }, + "inlineTemplate": { + "description": "Include template inline in the component.ts file. By default, an external template file is created and referenced in the component.ts file.", + "type": "boolean", + "default": false, + "alias": "t" + }, + "viewEncapsulation": { + "description": "The view encapsulation strategy to use in the new component.", + "enum": ["Emulated", "None", "ShadowDom"], + "type": "string", + "alias": "v" + }, + "changeDetection": { + "description": "The change detection strategy to use in the new component.", + "enum": ["Default", "OnPush"], + "type": "string", + "default": "Default", + "alias": "c" + }, + "style": { + "description": "The file extension or preprocessor to use for style files, or 'none' to skip generating the style file.", + "type": "string", + "default": "css", + "enum": ["css", "scss", "sass", "less", "none"] + }, + "skipTests": { + "type": "boolean", + "description": "Do not create \"spec.ts\" test files for the new component.", + "default": false + }, + "flat": { + "type": "boolean", + "description": "Create the new files at the top level of the current project.", + "default": false + }, + "selector": { + "type": "string", + "format": "html-selector", + "description": "The HTML selector to use for this component." + }, + "skipSelector": { + "type": "boolean", + "default": false, + "description": "Specifies if the component should have a selector or not." + }, + "type": { + "type": "string", + "description": "Adds a developer-defined type to the filename, in the format \"name.type.ts\".", + "default": "component" + }, + "export": { + "type": "boolean", + "description": "Specifies if the component should be exported in the declaring NgModule. Additionally, if the project is a library, the component will be exported from the project's entry point (normally `index.ts`).", + "default": false + } + }, + "required": ["name"] +} diff --git a/packages/angular/src/generators/scam-directive/lib/create-module.ts b/packages/angular/src/generators/scam-directive/lib/create-module.ts index 8422f5972f863..5500bd6e2439c 100644 --- a/packages/angular/src/generators/scam-directive/lib/create-module.ts +++ b/packages/angular/src/generators/scam-directive/lib/create-module.ts @@ -146,7 +146,7 @@ function exportScam(tree: Tree, schema: Schema, scamFilePath: string) { .split(sourceRoot)[1] .replace('.ts', '')}`; - const updateEntryPointContent = `${tree.read(projectEntryPoint)} + const updateEntryPointContent = `${tree.read(projectEntryPoint, 'utf-8')} export * from "${relativePathFromEntryPoint}";`; tree.write(projectEntryPoint, updateEntryPointContent); diff --git a/packages/angular/src/generators/scam-directive/scam-directive.ts b/packages/angular/src/generators/scam-directive/scam-directive.ts index 8a550d5949234..613006372d3f1 100644 --- a/packages/angular/src/generators/scam-directive/scam-directive.ts +++ b/packages/angular/src/generators/scam-directive/scam-directive.ts @@ -9,6 +9,7 @@ import { } from '@nrwl/devkit'; import { createScamDirective } from './lib/create-module'; import { normalize } from 'path'; +import { pathStartsWith } from '../utils/path'; export async function scamDirectiveGenerator(tree: Tree, schema: Schema) { const { inlineScam, ...options } = schema; @@ -44,7 +45,7 @@ function checkPathUnderProjectRoot(tree: Tree, options: Partial) { ? pathToDirective.slice(1) : pathToDirective; - if (!pathToDirective.startsWith(normalize(root))) { + if (!pathStartsWith(pathToDirective, root)) { throw new Error( `The path provided for the SCAM (${options.path}) does not exist under the project root (${root}).` ); diff --git a/packages/angular/src/generators/scam-pipe/lib/create-module.ts b/packages/angular/src/generators/scam-pipe/lib/create-module.ts index 397b1141ab094..9dcf1a705b10a 100644 --- a/packages/angular/src/generators/scam-pipe/lib/create-module.ts +++ b/packages/angular/src/generators/scam-pipe/lib/create-module.ts @@ -143,7 +143,7 @@ function exportScam(tree: Tree, schema: Schema, scamFilePath: string) { .split(sourceRoot)[1] .replace('.ts', '')}`; - const updateEntryPointContent = `${tree.read(projectEntryPoint)} + const updateEntryPointContent = `${tree.read(projectEntryPoint, 'utf-8')} export * from "${relativePathFromEntryPoint}";`; tree.write(projectEntryPoint, updateEntryPointContent); diff --git a/packages/angular/src/generators/scam-pipe/scam-pipe.ts b/packages/angular/src/generators/scam-pipe/scam-pipe.ts index e0eb7f5c71057..978d94ce9e7ac 100644 --- a/packages/angular/src/generators/scam-pipe/scam-pipe.ts +++ b/packages/angular/src/generators/scam-pipe/scam-pipe.ts @@ -9,6 +9,7 @@ import { } from '@nrwl/devkit'; import { createScamPipe } from './lib/create-module'; import { normalize } from 'path'; +import { pathStartsWith } from '../utils/path'; export async function scamPipeGenerator(tree: Tree, schema: Schema) { const { inlineScam, ...options } = schema; @@ -42,7 +43,7 @@ function checkPathUnderProjectRoot(tree: Tree, options: Partial) { let pathToPipe = normalizePath(options.path); pathToPipe = pathToPipe.startsWith('/') ? pathToPipe.slice(1) : pathToPipe; - if (!pathToPipe.startsWith(normalize(root))) { + if (!pathStartsWith(pathToPipe, root)) { throw new Error( `The path provided for the SCAM (${options.path}) does not exist under the project root (${root}).` ); diff --git a/packages/angular/src/generators/scam/lib/create-module.ts b/packages/angular/src/generators/scam/lib/create-module.ts index 7cc2ff6780849..e99da3f2f482f 100644 --- a/packages/angular/src/generators/scam/lib/create-module.ts +++ b/packages/angular/src/generators/scam/lib/create-module.ts @@ -148,7 +148,7 @@ function exportScam(tree: Tree, schema: Schema, scamFilePath: string) { .split(sourceRoot)[1] .replace('.ts', '')}`; - const updateEntryPointContent = `${tree.read(projectEntryPoint)} + const updateEntryPointContent = `${tree.read(projectEntryPoint, 'utf-8')} export * from "${relativePathFromEntryPoint}";`; tree.write(projectEntryPoint, updateEntryPointContent); diff --git a/packages/angular/src/generators/scam/scam.ts b/packages/angular/src/generators/scam/scam.ts index be5cbec4770d4..d7917d06d9352 100644 --- a/packages/angular/src/generators/scam/scam.ts +++ b/packages/angular/src/generators/scam/scam.ts @@ -8,7 +8,7 @@ import { normalizePath, } from '@nrwl/devkit'; import { createScam } from './lib/create-module'; -import { normalize } from 'path'; +import { pathStartsWith } from '../utils/path'; export async function scamGenerator(tree: Tree, schema: Schema) { const { inlineScam, ...options } = schema; @@ -44,7 +44,7 @@ function checkPathUnderProjectRoot(tree: Tree, options: Partial) { ? pathToComponent.slice(1) : pathToComponent; - if (!pathToComponent.startsWith(normalize(root))) { + if (!pathStartsWith(pathToComponent, root)) { throw new Error( `The path provided for the SCAM (${options.path}) does not exist under the project root (${root}).` ); diff --git a/packages/angular/src/generators/utils/path.spec.ts b/packages/angular/src/generators/utils/path.spec.ts new file mode 100644 index 0000000000000..1943ba56f72de --- /dev/null +++ b/packages/angular/src/generators/utils/path.spec.ts @@ -0,0 +1,17 @@ +import { pathStartsWith } from './path'; + +describe('path helpers', () => { + describe('pathStartsWith', () => { + it('should return true, regardless of OS path type', () => { + expect(pathStartsWith(`./lib1/src/app`, `lib1\\src\\app`)).toBeTruthy(); + }); + + it('should return false, regardless of OS path type, when path is wrong', () => { + expect(pathStartsWith(`./lib1/src/app`, `app1\\src\\app`)).toBeFalsy(); + }); + + it('should return true regardless if path has relative path chars', () => { + expect(pathStartsWith(`lib1/src/app`, `./lib1/src/app`)).toBeTruthy(); + }); + }); +}); diff --git a/packages/angular/src/generators/utils/path.ts b/packages/angular/src/generators/utils/path.ts new file mode 100644 index 0000000000000..2a4954f39227f --- /dev/null +++ b/packages/angular/src/generators/utils/path.ts @@ -0,0 +1,8 @@ +import { normalize } from 'path'; + +export function pathStartsWith(path1: string, path2: string) { + path1 = normalize(path1).replace(/\\/g, '/'); + path2 = normalize(path2).replace(/\\/g, '/'); + + return path1.startsWith(path2); +}