diff --git a/docs/generated/packages/angular/generators/component.json b/docs/generated/packages/angular/generators/component.json index 0d404b9450851..65a4cb9b13a3f 100644 --- a/docs/generated/packages/angular/generators/component.json +++ b/docs/generated/packages/angular/generators/component.json @@ -12,7 +12,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the component file path?" }, @@ -117,7 +117,7 @@ } }, "required": ["path"], - "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Component\" %}\n\nGenerate a component named `MyComponent` at `apps/my-app/src/lib/my-component/my-component.component.ts`:\n\n```bash\nnx g @nx/angular:component apps/my-app/src/lib/my-component/my-component\n```\n\n{% /tab %}\n\n{% tab label=\"With Different Symbol Name\" %}\n\nGenerate a component named `CustomComponent` at `apps/my-app/src/lib/my-component/my-component.component.ts`:\n\n```bash\nnx g @nx/angular:component apps/my-app/src/lib/my-component/my-component --name=custom\n```\n\n{% /tab %}\n\n{% tab label=\"Single File Component\" %}\n\nCreate a component named `my-component` with inline styles and inline template:\n\n```bash\nnx g @nx/angular:component apps/my-app/src/lib/my-component/my-component --inlineStyle --inlineTemplate\n```\n\n{% /tab %}\n\n{% tab label=\"Component with OnPush Change Detection Strategy\" %}\n\nCreate a component named `my-component` with OnPush Change Detection Strategy:\n\n```bash\nnx g @nx/angular:component apps/my-app/src/lib/my-component/my-component --changeDetection=OnPush\n```\n\n{% /tab %}\n", + "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Component\" %}\n\nGenerate a component named `MyComponent` at `apps/my-app/src/lib/my-component/my-component.component.ts`:\n\n```bash\nnx g @nx/angular:component apps/my-app/src/lib/my-component/my-component.ts\n```\n\n{% /tab %}\n\n{% tab label=\"Without Providing the File Extension\" %}\n\nGenerate a component named `MyComponent` at `apps/my-app/src/lib/my-component/my-component.component.ts`:\n\n```bash\nnx g @nx/angular:component apps/my-app/src/lib/my-component/my-component\n```\n\n{% /tab %}\n\n{% tab label=\"With Different Symbol Name\" %}\n\nGenerate a component named `CustomComponent` at `apps/my-app/src/lib/my-component/my-component.component.ts`:\n\n```bash\nnx g @nx/angular:component apps/my-app/src/lib/my-component/my-component --name=custom\n```\n\n{% /tab %}\n\n{% tab label=\"Single File Component\" %}\n\nCreate a component named `my-component` with inline styles and inline template:\n\n```bash\nnx g @nx/angular:component apps/my-app/src/lib/my-component/my-component --inlineStyle --inlineTemplate\n```\n\n{% /tab %}\n\n{% tab label=\"Component with OnPush Change Detection Strategy\" %}\n\nCreate a component named `my-component` with OnPush Change Detection Strategy:\n\n```bash\nnx g @nx/angular:component apps/my-app/src/lib/my-component/my-component --changeDetection=OnPush\n```\n\n{% /tab %}\n", "presets": [] }, "aliases": ["c"], diff --git a/docs/generated/packages/angular/generators/directive.json b/docs/generated/packages/angular/generators/directive.json index 8bf52bce9760c..6cab845005865 100644 --- a/docs/generated/packages/angular/generators/directive.json +++ b/docs/generated/packages/angular/generators/directive.json @@ -12,6 +12,10 @@ "examples": [ { "description": "Generate a directive with the exported symbol matching the file name. It results in the directive `FooDirective` at `mylib/src/lib/foo.directive.ts`", + "command": "nx g @nx/angular:directive mylib/src/lib/foo.directive.ts" + }, + { + "description": "Generate a directive without providing the file extension. It results in the directive `FooDirective` at `mylib/src/lib/foo.directive.ts`", "command": "nx g @nx/angular:directive mylib/src/lib/foo" }, { @@ -22,7 +26,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the directive without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the directive. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the directive file path?" }, diff --git a/docs/generated/packages/angular/generators/pipe.json b/docs/generated/packages/angular/generators/pipe.json index 8f01183961b94..d2bdb46f562cc 100644 --- a/docs/generated/packages/angular/generators/pipe.json +++ b/docs/generated/packages/angular/generators/pipe.json @@ -12,6 +12,10 @@ "examples": [ { "description": "Generate a pipe with the exported symbol matching the file name. It results in the pipe `FooPipe` at `mylib/src/lib/foo.pipe.ts`", + "command": "nx g @nx/angular:pipe mylib/src/lib/foo.pipe.ts" + }, + { + "description": "Generate a pipe without providing the file extension. It results in the pipe `FooPipe` at `mylib/src/lib/foo.pipe.ts`", "command": "nx g @nx/angular:pipe mylib/src/lib/foo" }, { @@ -22,7 +26,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the pipe without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the pipe. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the pipe file path?" }, diff --git a/docs/generated/packages/angular/generators/scam-directive.json b/docs/generated/packages/angular/generators/scam-directive.json index b1d5db9ec57dd..a6bd20adf5a8e 100644 --- a/docs/generated/packages/angular/generators/scam-directive.json +++ b/docs/generated/packages/angular/generators/scam-directive.json @@ -10,6 +10,10 @@ "examples": [ { "description": "Generate a directive with the exported symbol matching the file name. It results in the directive `FooDirective` at `mylib/src/lib/foo.directive.ts`", + "command": "nx g @nx/angular:scam-directive mylib/src/lib/foo.directive.ts" + }, + { + "description": "Generate a directive without providing the file extension. It results in the directive `FooDirective` at `mylib/src/lib/foo.directive.ts`", "command": "nx g @nx/angular:scam-directive mylib/src/lib/foo" }, { @@ -22,7 +26,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the SCAM directive without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the SCAM directive. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the SCAM directive file path?" }, diff --git a/docs/generated/packages/angular/generators/scam-pipe.json b/docs/generated/packages/angular/generators/scam-pipe.json index a12009cac3964..96035de5dfafc 100644 --- a/docs/generated/packages/angular/generators/scam-pipe.json +++ b/docs/generated/packages/angular/generators/scam-pipe.json @@ -10,6 +10,10 @@ "examples": [ { "description": "Generate a pipe with the exported symbol matching the file name. It results in the pipe `FooPipe` at `mylib/src/lib/foo.pipe.ts`", + "command": "nx g @nx/angular:scam-pipe mylib/src/lib/foo.pipe.ts" + }, + { + "description": "Generate a pipe without providing the file extension. It results in the pipe `FooPipe` at `mylib/src/lib/foo.pipe.ts`", "command": "nx g @nx/angular:scam-pipe mylib/src/lib/foo" }, { @@ -22,7 +26,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the SCAM pipe without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the SCAM pipe. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the SCAM pipe file path?" }, diff --git a/docs/generated/packages/angular/generators/scam.json b/docs/generated/packages/angular/generators/scam.json index 0bc374ec6e408..418d31141400b 100644 --- a/docs/generated/packages/angular/generators/scam.json +++ b/docs/generated/packages/angular/generators/scam.json @@ -10,6 +10,10 @@ "examples": [ { "description": "Generate a component with the exported symbol matching the file name. It results in the component `FooComponent` at `mylib/src/lib/foo.component.ts`", + "command": "nx g @nx/angular:scam mylib/src/lib/foo.component.ts" + }, + { + "description": "Generate a component without providing the file extension. It results in the component `FooComponent` at `mylib/src/lib/foo.component.ts`", "command": "nx g @nx/angular:scam mylib/src/lib/foo" }, { @@ -22,7 +26,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the SCAM without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the SCAM. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the SCAM file path?" }, diff --git a/docs/generated/packages/expo/generators/component.json b/docs/generated/packages/expo/generators/component.json index e89226db41cc5..bd1b8b721e68e 100644 --- a/docs/generated/packages/expo/generators/component.json +++ b/docs/generated/packages/expo/generators/component.json @@ -10,11 +10,15 @@ "examples": [ { "description": "Generate a component with the exported symbol matching the file name. It results in the component `Foo` at `mylib/src/foo.tsx`", - "command": "nx g @nx/expo:component mylib/src/foo" + "command": "nx g @nx/expo:component mylib/src/foo.tsx" }, { "description": "Generate a component with the exported symbol different from the file name. It results in the component `Custom` at `mylib/src/foo.tsx`", - "command": "nx g @nx/expo:component mylib/src/foo --name=custom" + "command": "nx g @nx/expo:component mylib/src/foo.tsx --name=custom" + }, + { + "description": "Generate a component without the providing the file extension. It results in the component `Foo` at `mylib/src/foo.tsx`", + "command": "nx g @nx/expo:component mylib/src/foo" }, { "description": "Generate a class component at `mylib/src/foo.tsx`", @@ -24,7 +28,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the component file path?" }, @@ -35,7 +39,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipFormat": { "description": "Skip formatting files.", diff --git a/docs/generated/packages/nest/generators/class.json b/docs/generated/packages/nest/generators/class.json index 8cb77da4265d6..847ca046c13d0 100644 --- a/docs/generated/packages/nest/generators/class.json +++ b/docs/generated/packages/nest/generators/class.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the class `Foo` at `myapp/src/app/foo.ts`", + "command": "nx g @nx/nest:class myapp/src/app/foo.ts" + }, + { + "description": "Generate the class without providing the file extension. It results in the class `Foo` at `myapp/src/app/foo.ts`", "command": "nx g @nx/nest:class myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the class without the file extension. Relative to the current working directory.", + "description": "The file path to the class. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the class file path?" diff --git a/docs/generated/packages/nest/generators/controller.json b/docs/generated/packages/nest/generators/controller.json index 58c64fccc5b1a..ba7e2c2b15d1c 100644 --- a/docs/generated/packages/nest/generators/controller.json +++ b/docs/generated/packages/nest/generators/controller.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the controller `FooController` at `myapp/src/app/foo.controller.ts`", + "command": "nx g @nx/nest:controller myapp/src/app/foo.controller.ts" + }, + { + "description": "Generate the controller without providing the file extension. It results in the controller `FooController` at `myapp/src/app/foo.controller.ts`", "command": "nx g @nx/nest:controller myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the controller without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the controller. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the controller file path?" diff --git a/docs/generated/packages/nest/generators/decorator.json b/docs/generated/packages/nest/generators/decorator.json index 218a1b364dc1c..6fed26b6f4058 100644 --- a/docs/generated/packages/nest/generators/decorator.json +++ b/docs/generated/packages/nest/generators/decorator.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the decorator `Foo` at `myapp/src/app/foo.decorator.ts`", + "command": "nx g @nx/nest:decorator myapp/src/app/foo.decorator.ts" + }, + { + "description": "Generate the decorator without providing the file extension. It results in the decorator `Foo` at `myapp/src/app/foo.decorator.ts`", "command": "nx g @nx/nest:decorator myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the decorator without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the decorator. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the decorator file path?" diff --git a/docs/generated/packages/nest/generators/filter.json b/docs/generated/packages/nest/generators/filter.json index 02c70a46572f9..567513deb7ab7 100644 --- a/docs/generated/packages/nest/generators/filter.json +++ b/docs/generated/packages/nest/generators/filter.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the filter `FooFilter` at `myapp/src/app/foo.filter.ts`", + "command": "nx g @nx/nest:filter myapp/src/app/foo.filter.ts" + }, + { + "description": "Generate the filter without providing the file extension. It results in the filter `FooFilter` at `myapp/src/app/foo.filter.ts`", "command": "nx g @nx/nest:filter myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the filter without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the filter. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the filter file path?" diff --git a/docs/generated/packages/nest/generators/gateway.json b/docs/generated/packages/nest/generators/gateway.json index 54899663ce7b5..85f21f794b5c4 100644 --- a/docs/generated/packages/nest/generators/gateway.json +++ b/docs/generated/packages/nest/generators/gateway.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the gateway `FooGateway` at `myapp/src/app/foo.gateway.ts`", + "command": "nx g @nx/nest:gateway myapp/src/app/foo.gateway.ts" + }, + { + "description": "Generate the gateway without providing the file extension. It results in the gateway `FooGateway` at `myapp/src/app/foo.gateway.ts`", "command": "nx g @nx/nest:gateway myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the gateway without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the gateway. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the gateway file path?" diff --git a/docs/generated/packages/nest/generators/guard.json b/docs/generated/packages/nest/generators/guard.json index 4731a5d9a2045..03a316bc4d7e4 100644 --- a/docs/generated/packages/nest/generators/guard.json +++ b/docs/generated/packages/nest/generators/guard.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the guard `FooGuard` at `myapp/src/app/foo.guard.ts`", + "command": "nx g @nx/nest:guard myapp/src/app/foo.guard.ts" + }, + { + "description": "Generate the guard without providing the file extension. It results in the guard `FooGuard` at `myapp/src/app/foo.guard.ts`", "command": "nx g @nx/nest:guard myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the guard without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the guard. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the guard file path?" diff --git a/docs/generated/packages/nest/generators/interceptor.json b/docs/generated/packages/nest/generators/interceptor.json index 656d5f9ce4fa4..adc4d3d3fabcc 100644 --- a/docs/generated/packages/nest/generators/interceptor.json +++ b/docs/generated/packages/nest/generators/interceptor.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the interceptor `FooInterceptor` at `myapp/src/app/foo.interceptor.ts`", + "command": "nx g @nx/nest:interceptor myapp/src/app/foo.interceptor.ts" + }, + { + "description": "Generate the interceptor without providing the file extension. It results in the interceptor `FooInterceptor` at `myapp/src/app/foo.interceptor.ts`", "command": "nx g @nx/nest:interceptor myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the interceptor without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the interceptor. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the interceptor file path?" diff --git a/docs/generated/packages/nest/generators/interface.json b/docs/generated/packages/nest/generators/interface.json index a9cba7590699c..eb6b07829c5ac 100644 --- a/docs/generated/packages/nest/generators/interface.json +++ b/docs/generated/packages/nest/generators/interface.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the interface `Foo` at `myapp/src/app/foo.interface.ts`", + "command": "nx g @nx/nest:interface myapp/src/app/foo.interface.ts" + }, + { + "description": "Generate the interface without providing the file extension. It results in the interface `Foo` at `myapp/src/app/foo.interface.ts`", "command": "nx g @nx/nest:interface myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the interface without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the interface. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the interface file path?" diff --git a/docs/generated/packages/nest/generators/middleware.json b/docs/generated/packages/nest/generators/middleware.json index 23bcfd22b9083..3ca6603ea1cb5 100644 --- a/docs/generated/packages/nest/generators/middleware.json +++ b/docs/generated/packages/nest/generators/middleware.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the middleware `FooMiddleware` at `myapp/src/app/foo.middleware.ts`", + "command": "nx g @nx/nest:middleware myapp/src/app/foo.middleware.ts" + }, + { + "description": "Generate the middleware without providing the file extension. It results in the middleware `FooMiddleware` at `myapp/src/app/foo.middleware.ts`", "command": "nx g @nx/nest:middleware myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the middleware without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the middleware. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the middleware file path?" diff --git a/docs/generated/packages/nest/generators/module.json b/docs/generated/packages/nest/generators/module.json index 40c3e0aaf07b6..357da0969d471 100644 --- a/docs/generated/packages/nest/generators/module.json +++ b/docs/generated/packages/nest/generators/module.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the module `FooModule` at `myapp/src/app/foo.module.ts`", + "command": "nx g @nx/nest:module myapp/src/app/foo.module.ts" + }, + { + "description": "Generate the module without providing the file extension. It results in the module `FooModule` at `myapp/src/app/foo.module.ts`", "command": "nx g @nx/nest:module myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the module without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the module. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the module file path?" diff --git a/docs/generated/packages/nest/generators/pipe.json b/docs/generated/packages/nest/generators/pipe.json index e35abd415bd78..fb32f8eefb70d 100644 --- a/docs/generated/packages/nest/generators/pipe.json +++ b/docs/generated/packages/nest/generators/pipe.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the pipe `FooPipe` at `myapp/src/app/foo.pipe.ts`", + "command": "nx g @nx/nest:pipe myapp/src/app/foo.pipe.ts" + }, + { + "description": "Generate the pipe without providing the file extension. It results in the pipe `FooPipe` at `myapp/src/app/foo.pipe.ts`", "command": "nx g @nx/nest:pipe myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the pipe without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the pipe. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the pipe file path?" diff --git a/docs/generated/packages/nest/generators/provider.json b/docs/generated/packages/nest/generators/provider.json index afa111b68ef87..faeedb6261bc1 100644 --- a/docs/generated/packages/nest/generators/provider.json +++ b/docs/generated/packages/nest/generators/provider.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the provider `Foo` at `myapp/src/app/foo.ts`", + "command": "nx g @nx/nest:provider myapp/src/app/foo.ts" + }, + { + "description": "Generate the provider without providing the file extension. It results in the provider `Foo` at `myapp/src/app/foo.ts`", "command": "nx g @nx/nest:provider myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the provider without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the provider. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the provider file path?" diff --git a/docs/generated/packages/nest/generators/resolver.json b/docs/generated/packages/nest/generators/resolver.json index 83aed6442ff14..a09b09f6f14c7 100644 --- a/docs/generated/packages/nest/generators/resolver.json +++ b/docs/generated/packages/nest/generators/resolver.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the resolver `FooResolver` at `myapp/src/app/foo.resolver.ts`", + "command": "nx g @nx/nest:resolver myapp/src/app/foo.resolver.ts" + }, + { + "description": "Generate the resolver without providing the file extension. It results in the resolver `FooResolver` at `myapp/src/app/foo.resolver.ts`", "command": "nx g @nx/nest:resolver myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the resolver without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the resolver. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the resolver file path?" diff --git a/docs/generated/packages/nest/generators/resource.json b/docs/generated/packages/nest/generators/resource.json index 083e6135cc869..db583736d2ebb 100644 --- a/docs/generated/packages/nest/generators/resource.json +++ b/docs/generated/packages/nest/generators/resource.json @@ -17,7 +17,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the resource without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the resource. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the resource file path?" }, @@ -33,11 +33,6 @@ "enum": ["jest", "none"], "default": "jest" }, - "language": { - "description": "Nest class language.", - "type": "string", - "enum": ["js", "ts"] - }, "type": { "type": "string", "description": "The transport layer.", diff --git a/docs/generated/packages/nest/generators/service.json b/docs/generated/packages/nest/generators/service.json index f55e04fbb4d2b..50a609ccfda66 100644 --- a/docs/generated/packages/nest/generators/service.json +++ b/docs/generated/packages/nest/generators/service.json @@ -11,12 +11,16 @@ "examples": [ { "description": "Generate the service `FooService` at `myapp/src/app/foo.service.ts`", + "command": "nx g @nx/nest:service myapp/src/app/foo.service.ts" + }, + { + "description": "Generate the service without providing the file extension. It results in the service `FooService` at `myapp/src/app/foo.service.ts`", "command": "nx g @nx/nest:service myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the service without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the service. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the service file path?" diff --git a/docs/generated/packages/next/generators/component.json b/docs/generated/packages/next/generators/component.json index be5ecf0e4b594..f21be28991641 100644 --- a/docs/generated/packages/next/generators/component.json +++ b/docs/generated/packages/next/generators/component.json @@ -11,7 +11,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the component file path?", "x-priority": "important" @@ -69,7 +69,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipFormat": { "description": "Skip formatting files.", @@ -79,7 +79,7 @@ } }, "required": ["path"], - "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Create a Component\" %}\n\nGenerate a component named `MyComponent` at `apps/my-app/src/app/my-component/my-component.tsx`:\n\n```shell\nnx g component apps/my-app/src/app/my-component/my-component\n```\n\n{% /tab %}\n{% tab label=\"Create a Component with a Different Symbol Name\" %}\n\nGenerate a component named `Custom` at `apps/my-app/src/app/my-component/my-component.tsx`:\n\n```shell\nnx g component apps/my-app/src/app/my-component/my-component --name=custom\n```\n\n{% /tab %}\n{% /tabs %}\n", + "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Create a Component\" %}\n\nGenerate a component named `MyComponent` at `apps/my-app/src/app/my-component/my-component.tsx`:\n\n```shell\nnx g component apps/my-app/src/app/my-component/my-component.tsx\n```\n\n{% /tab %}\n{% tab label=\"Create a Component with a Different Symbol Name\" %}\n\nGenerate a component named `Custom` at `apps/my-app/src/app/my-component/my-component.tsx`:\n\n```shell\nnx g component apps/my-app/src/app/my-component/my-component.tsx --name=custom\n```\n\n{% /tab %}\n{% tab label=\"Create a Component Omitting the File Extension\" %}\n\nGenerate a component named `MyComponent` at `apps/my-app/src/app/my-component/my-component.tsx` without specifying the file extension:\n\n```shell\nnx g component apps/my-app/src/app/my-component/my-component\n```\n\n{% /tab %}\n{% /tabs %}\n", "presets": [] }, "description": "Create a component.", diff --git a/docs/generated/packages/plugin/generators/executor.json b/docs/generated/packages/plugin/generators/executor.json index afbc23662ccd6..fbab6e9ae2380 100644 --- a/docs/generated/packages/plugin/generators/executor.json +++ b/docs/generated/packages/plugin/generators/executor.json @@ -7,12 +7,12 @@ "$id": "NxPluginExecutor", "title": "Create an Executor for an Nx Plugin", "description": "Create an Executor for an Nx Plugin.", - "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Basic executor\" %}\n\nCreate a new executor called `build` at `tools/my-plugin/src/executors/build.ts`:\n\n```bash\nnx g @nx/plugin:executor tools/my-plugin/src/executors/build\n```\n\n{% /tab %}\n{% tab label=\"With different exported name\" %}\n\nCreate a new executor called `custom` at `tools/my-plugin/src/executors/build.ts`:\n\n```bash\nnx g @nx/plugin:executor tools/my-plugin/src/executors/build --name=custom\n```\n\n{% /tab %}\n{% tab label=\"With custom hashing\" %}\n\nCreate a new executor called `build` at `tools/my-plugin/src/executors/build.ts`, that uses a custom hashing function:\n\n```bash\nnx g @nx/plugin:executor tools/my-plugin/src/executors/build --includeHasher\n```\n\n{% /tab %}\n{% /tabs %}\n", + "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Basic executor\" %}\n\nCreate a new executor called `build` at `tools/my-plugin/src/executors/build.ts`:\n\n```bash\nnx g @nx/plugin:executor tools/my-plugin/src/executors/build.ts\n```\n\n{% /tab %}\n{% tab label=\"Without providing the file extension\" %}\n\nCreate a new executor called `build` at `tools/my-plugin/src/executors/build.ts`:\n\n```bash\nnx g @nx/plugin:executor tools/my-plugin/src/executors/build\n```\n\n{% /tab %}\n{% tab label=\"With different exported name\" %}\n\nCreate a new executor called `custom` at `tools/my-plugin/src/executors/build.ts`:\n\n```bash\nnx g @nx/plugin:executor tools/my-plugin/src/executors/build.ts --name=custom\n```\n\n{% /tab %}\n{% tab label=\"With custom hashing\" %}\n\nCreate a new executor called `build` at `tools/my-plugin/src/executors/build.ts`, that uses a custom hashing function:\n\n```bash\nnx g @nx/plugin:executor tools/my-plugin/src/executors/build --includeHasher\n```\n\n{% /tab %}\n{% /tabs %}\n", "type": "object", "properties": { "path": { "type": "string", - "description": "The file path to the executor without the file extension. Relative to the current working directory.", + "description": "The file path to the executor. Relative to the current working directory.", "x-prompt": "What is the executor file path?", "$default": { "$source": "argv", "index": 0 }, "x-priority": "important" diff --git a/docs/generated/packages/plugin/generators/generator.json b/docs/generated/packages/plugin/generators/generator.json index 37f5350d58118..6f22046feeef9 100644 --- a/docs/generated/packages/plugin/generators/generator.json +++ b/docs/generated/packages/plugin/generators/generator.json @@ -11,6 +11,10 @@ "examples": [ { "description": "Generate a generator exported with the name matching the file name. It results in the generator `foo` at `mylib/src/generators/foo.ts`", + "command": "nx g @nx/plugin:generator mylib/src/generators/foo.ts" + }, + { + "description": "Generate a generator without providing the file extension. It results in the generator `foo` at `mylib/src/generators/foo.ts`", "command": "nx g @nx/plugin:generator mylib/src/generators/foo" }, { @@ -21,7 +25,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the generator without the file extension. Relative to the current working directory.", + "description": "The file path to the generator. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the generator file path?", "x-priority": "important" diff --git a/docs/generated/packages/plugin/generators/migration.json b/docs/generated/packages/plugin/generators/migration.json index e86d1e1f0da72..5af45b2bf3cbf 100644 --- a/docs/generated/packages/plugin/generators/migration.json +++ b/docs/generated/packages/plugin/generators/migration.json @@ -11,6 +11,10 @@ "examples": [ { "description": "Generate a migration exported with the name matching the file name, which will be triggered when migrating to version 1.0.0 or above from a previous version. It results in the migration `foo` at `mylib/src/migrations/foo.ts`", + "command": "nx g @nx/plugin:migration mylib/src/migrations/foo.ts -v=1.0.0" + }, + { + "description": "Generate a migration without providing the file extension, which will be triggered when migrating to version 1.0.0 or above from a previous version. It results in the migration `foo` at `mylib/src/migrations/foo.ts`", "command": "nx g @nx/plugin:migration mylib/src/migrations/foo -v=1.0.0" }, { diff --git a/docs/generated/packages/react-native/generators/component.json b/docs/generated/packages/react-native/generators/component.json index b8110eae0c65b..1e51f213cc76c 100644 --- a/docs/generated/packages/react-native/generators/component.json +++ b/docs/generated/packages/react-native/generators/component.json @@ -11,11 +11,15 @@ "examples": [ { "description": "Generate a component with the exported symbol matching the file name. It results in the component `Foo` at `mylib/src/lib/foo.tsx`", - "command": "nx g @nx/react-native:component mylib/src/lib/foo" + "command": "nx g @nx/react-native:component mylib/src/lib/foo.tsx" }, { "description": "Generate a component with the exported symbol different from the file name. It results in the component `Custom` at `mylib/src/lib/foo.tsx`", - "command": "nx g @nx/react-native:component mylib/src/lib/foo --name=custom" + "command": "nx g @nx/react-native:component mylib/src/lib/foo.tsx --name=custom" + }, + { + "description": "Generate a component without providing the file extension. It results in the component `Foo` at `mylib/src/lib/foo.tsx`", + "command": "nx g @nx/react-native:component mylib/src/lib/foo" }, { "description": "Generate a class component at `mylib/src/lib/foo.tsx`", @@ -25,7 +29,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the component file path?" }, @@ -36,7 +40,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipTests": { "type": "boolean", diff --git a/docs/generated/packages/react/generators/component.json b/docs/generated/packages/react/generators/component.json index 53432b4bc212a..e1fb4b811172e 100644 --- a/docs/generated/packages/react/generators/component.json +++ b/docs/generated/packages/react/generators/component.json @@ -11,7 +11,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the component file path?", "x-priority": "important" @@ -57,7 +57,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipTests": { "type": "boolean", @@ -87,10 +87,6 @@ "description": "Default is `false`. When `true`, the component is generated with `*.css`/`*.scss` instead of `*.module.css`/`*.module.scss`.", "default": false }, - "fileName": { - "type": "string", - "description": "Create a component with this file name." - }, "inSourceTests": { "type": "boolean", "default": false, @@ -104,7 +100,7 @@ } }, "required": ["path"], - "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Component\" %}\n\nCreate a component named `MyComponent` at `libs/ui/src/my-component.tsx`:\n\n```shell\nnx g @nx/react:component libs/ui/src/my-component\n```\n\n{% /tab %}\n\n{% tab label=\"With a Different Symbol Name\" %}\n\nCreate a component named `Custom` at `libs/ui/src/my-component.tsx`:\n\n```shell\nnx g @nx/react:component libs/ui/src/my-component --name=custom\n```\n\n{% /tab %}\n\n{% tab label=\"Class Component\" %}\n\nCreate a class component named `MyComponent` at `libs/ui/src/my-component.tsx`:\n\n```shell\nnx g @nx/react:component libs/ui/src/my-component --classComponent\n```\n\n{% /tab %}\n", + "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Component\" %}\n\nCreate a component named `MyComponent` at `libs/ui/src/my-component.tsx`:\n\n```shell\nnx g @nx/react:component libs/ui/src/my-component.tsx\n```\n\n{% /tab %}\n\n{% tab label=\"With a Different Symbol Name\" %}\n\nCreate a component named `Custom` at `libs/ui/src/my-component.tsx`:\n\n```shell\nnx g @nx/react:component libs/ui/src/my-component.tsx --name=custom\n```\n\n{% /tab %}\n\n{% tab label=\"Omitting the File Extension\" %}\n\nCreate a component named `MyComponent` at `libs/ui/src/my-component.tsx` without specifying the file extension:\n\n```shell\nnx g @nx/react:component libs/ui/src/my-component\n```\n\n{% /tab %}\n\n{% tab label=\"Class Component\" %}\n\nCreate a class component named `MyComponent` at `libs/ui/src/my-component.tsx`:\n\n```shell\nnx g @nx/react:component libs/ui/src/my-component --classComponent\n```\n\n{% /tab %}\n", "presets": [] }, "description": "Create a React component.", diff --git a/docs/generated/packages/react/generators/hook.json b/docs/generated/packages/react/generators/hook.json index e95adfe3f908d..f5e65b4cbf2ff 100644 --- a/docs/generated/packages/react/generators/hook.json +++ b/docs/generated/packages/react/generators/hook.json @@ -11,17 +11,21 @@ "examples": [ { "description": "Generate a hook with the exported symbol matching the file name. It results in the hook `useFoo` at `mylib/src/lib/foo.ts`", - "command": "nx g @nx/react:hook mylib/src/lib/foo" + "command": "nx g @nx/react:hook mylib/src/lib/foo.ts" }, { "description": "Generate a hook with the exported symbol different from the file name. It results in the hook `useCustom` at `mylib/src/lib/foo.ts`", - "command": "nx g @nx/react:hook mylib/src/lib/foo --name=useCustom" + "command": "nx g @nx/react:hook mylib/src/lib/foo.ts --name=useCustom" + }, + { + "description": "Generate a hook without providing the file extension. It results in the hook `useFoo` at `mylib/src/lib/foo.ts`", + "command": "nx g @nx/react:hook mylib/src/lib/foo" } ], "properties": { "path": { "type": "string", - "description": "The file path to the hook without the file extension. Relative to the current working directory.", + "description": "The file path to the hook. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the hook file path?", "x-priority": "important" @@ -33,7 +37,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipTests": { "type": "boolean", diff --git a/docs/generated/packages/react/generators/redux.json b/docs/generated/packages/react/generators/redux.json index 893c4845dc2df..e031301126e57 100644 --- a/docs/generated/packages/react/generators/redux.json +++ b/docs/generated/packages/react/generators/redux.json @@ -11,17 +11,21 @@ "examples": [ { "description": "Generate a Redux state slice with the exported symbol matching the file name. It results in the slice `fooSlice` at `mylib/src/lib/foo.slice.ts`", - "command": "nx g @nx/react:redux mylib/src/lib/foo" + "command": "nx g @nx/react:redux mylib/src/lib/foo.slice.ts" }, { "description": "Generate a Redux state slice with the exported symbol different from the file name. It results in the slice `customSlice` at `mylib/src/lib/foo.slice.ts`", - "command": "nx g @nx/react:redux mylib/src/lib/foo --name=custom" + "command": "nx g @nx/react:redux mylib/src/lib/foo.slice.ts --name=custom" + }, + { + "description": "Generate a Redux state slice without providing the \"slice\" suffix and the file extension. It results in the slice `fooSlice` at `mylib/src/lib/foo.slice.ts`", + "command": "nx g @nx/react:redux mylib/src/lib/foo" } ], "properties": { "path": { "type": "string", - "description": "The file path to the Redux state slice without the file extension. Relative to the current working directory.", + "description": "The file path to the Redux state slice. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the Redux stateslice file path?", "x-priority": "important" @@ -38,7 +42,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." } }, "required": ["path"], diff --git a/docs/generated/packages/remix/generators/resource-route.json b/docs/generated/packages/remix/generators/resource-route.json index dbf4176977287..372dcf4aab0ff 100644 --- a/docs/generated/packages/remix/generators/resource-route.json +++ b/docs/generated/packages/remix/generators/resource-route.json @@ -9,16 +9,20 @@ "description": "Generate a resource route.", "examples": [ { - "command": "g resource-route 'path/to/page'", - "description": "Generate resource route at /path/to/page" + "description": "Generate a resource route at `myapp/app/routes/foo.ts`", + "command": "nx g resource-route myapp/app/routes/foo.ts" + }, + { + "description": "Generate a resource route without providing the file extension at `myapp/app/routes/foo.tsx`", + "command": "nx g resource-route myapp/app/routes/foo" } ], "properties": { "path": { "type": "string", - "description": "The route path or path to the filename of the route.", + "description": "The file path to the route. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, - "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')" + "x-prompt": "What is the route file path?" }, "action": { "type": "boolean", diff --git a/docs/generated/packages/remix/generators/route.json b/docs/generated/packages/remix/generators/route.json index 650ecdef432e0..5ff734b159226 100644 --- a/docs/generated/packages/remix/generators/route.json +++ b/docs/generated/packages/remix/generators/route.json @@ -9,16 +9,20 @@ "type": "object", "examples": [ { - "command": "g route 'path/to/page'", - "description": "Generate route at /path/to/page" + "description": "Generate a route at `myapp/app/routes/foo.tsx`", + "command": "nx g resource-route myapp/app/routes/foo.tsx" + }, + { + "description": "Generate a route without providing the file extension at `myapp/app/routes/foo.tsx`", + "command": "nx g resource-route myapp/app/routes/foo" } ], "properties": { "path": { "type": "string", - "description": "The route path or path to the filename of the route.", + "description": "The file path to the route. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, - "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')" + "x-prompt": "What is the route file path?" }, "style": { "type": "string", diff --git a/docs/generated/packages/remix/generators/style.json b/docs/generated/packages/remix/generators/style.json index 6a4da8b768057..c4f07f8e932ed 100644 --- a/docs/generated/packages/remix/generators/style.json +++ b/docs/generated/packages/remix/generators/style.json @@ -9,16 +9,16 @@ "type": "object", "examples": [ { - "command": "g style --path='apps/demo/app/routes/path/to/page.tsx'", - "description": "Generate route at apps/demo/app/routes/path/to/page.tsx" + "description": "Generate a stylesheet at `myapp/app/styles/foo.css`", + "command": "nx g style myapp/app/routes/foo.tsx" } ], "properties": { "path": { "type": "string", - "description": "Route path", + "description": "The file path to the route. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, - "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + "x-prompt": "What is the route file path?" } }, "required": ["path"], diff --git a/docs/generated/packages/vue/generators/component.json b/docs/generated/packages/vue/generators/component.json index 6eb4e14372287..4df1282447ceb 100644 --- a/docs/generated/packages/vue/generators/component.json +++ b/docs/generated/packages/vue/generators/component.json @@ -9,6 +9,14 @@ "description": "Create a Vue Component for Nx.", "type": "object", "examples": [ + { + "description": "Generate a component at `mylib/src/lib/foo.vue`", + "command": "nx g @nx/vue:component mylib/src/lib/foo.vue" + }, + { + "description": "Generate a component without providing the file extension at `mylib/src/lib/foo.vue`", + "command": "nx g @nx/vue:component mylib/src/lib/foo" + }, { "description": "Generate a component at `mylib/src/lib/foo.vue` with `vitest` as the unit test runner", "command": "nx g @nx/vue:component mylib/src/lib/foo --unitTestRunner=vitest" @@ -17,7 +25,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the component file path?" }, diff --git a/packages/angular/docs/component-examples.md b/packages/angular/docs/component-examples.md index 08e0ae5a7dbdd..69959968ebaf7 100644 --- a/packages/angular/docs/component-examples.md +++ b/packages/angular/docs/component-examples.md @@ -5,6 +5,16 @@ Generate a component named `MyComponent` at `apps/my-app/src/lib/my-component/my-component.component.ts`: +```bash +nx g @nx/angular:component apps/my-app/src/lib/my-component/my-component.ts +``` + +{% /tab %} + +{% tab label="Without Providing the File Extension" %} + +Generate a component named `MyComponent` at `apps/my-app/src/lib/my-component/my-component.component.ts`: + ```bash nx g @nx/angular:component apps/my-app/src/lib/my-component/my-component ``` diff --git a/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap b/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap index b23378f46f6e5..66c814767c32c 100644 --- a/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap +++ b/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap @@ -179,6 +179,54 @@ export class ExampleComponent {} " `; +exports[`component Generator should handle path with file extension: component 1`] = ` +"import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'example.component', + imports: [CommonModule], + templateUrl: './example.component.html', + styleUrl: './example.component.css', +}) +export class ExampleComponentComponent {} +" +`; + +exports[`component Generator should handle path with file extension: component test file 1`] = ` +"import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ExampleComponentComponent } from './example.component'; + +describe('ExampleComponentComponent', () => { + let component: ExampleComponentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ExampleComponentComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ExampleComponentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); +" +`; + +exports[`component Generator should handle path with file extension: entry point file 1`] = `null`; + +exports[`component Generator should handle path with file extension: stylesheet 1`] = `""`; + +exports[`component Generator should handle path with file extension: template 1`] = ` +"

example.component works!

+" +`; + exports[`component Generator should inline styles when --inline-style=true 1`] = ` "import { Component } from '@angular/core'; diff --git a/packages/angular/src/generators/component/component.spec.ts b/packages/angular/src/generators/component/component.spec.ts index 065bec0a3fc00..0b2b7356a25d1 100644 --- a/packages/angular/src/generators/component/component.spec.ts +++ b/packages/angular/src/generators/component/component.spec.ts @@ -77,6 +77,35 @@ describe('component Generator', () => { ).toContain(`import ExampleComponent from './example.component';`); }); + it('should handle path with file extension', async () => { + const tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'lib1', { + projectType: 'library', + sourceRoot: 'lib1/src', + root: 'lib1', + }); + + await componentGenerator(tree, { + path: 'lib1/src/lib/example/example.component.ts', + }); + + expect( + tree.read('lib1/src/lib/example/example.component.ts', 'utf-8') + ).toMatchSnapshot('component'); + expect( + tree.read('lib1/src/lib/example/example.component.html', 'utf-8') + ).toMatchSnapshot('template'); + expect( + tree.read('lib1/src/lib/example/example.component.css', 'utf-8') + ).toMatchSnapshot('stylesheet'); + expect( + tree.read('lib1/src/lib/example/example.component.spec.ts', 'utf-8') + ).toMatchSnapshot('component test file'); + expect(tree.read('lib1/src/index.ts', 'utf-8')).toMatchSnapshot( + 'entry point file' + ); + }); + it('should not generate test file when --skip-tests=true', async () => { // ARRANGE const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); diff --git a/packages/angular/src/generators/component/lib/normalize-options.ts b/packages/angular/src/generators/component/lib/normalize-options.ts index 2618bf96886ae..eb1609b7b0540 100644 --- a/packages/angular/src/generators/component/lib/normalize-options.ts +++ b/packages/angular/src/generators/component/lib/normalize-options.ts @@ -21,6 +21,8 @@ export async function normalizeOptions( name: options.name, path: options.path, suffix: options.type ?? 'component', + allowedFileExtensions: ['ts'], + fileExtension: 'ts', }); const { className } = names(name); diff --git a/packages/angular/src/generators/component/schema.json b/packages/angular/src/generators/component/schema.json index e0c7dd09b3580..10f398eb13fe3 100644 --- a/packages/angular/src/generators/component/schema.json +++ b/packages/angular/src/generators/component/schema.json @@ -9,7 +9,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 diff --git a/packages/angular/src/generators/directive/__snapshots__/directive.spec.ts.snap b/packages/angular/src/generators/directive/__snapshots__/directive.spec.ts.snap index 5bfaba7d2dd26..9c4af9e590fa7 100644 --- a/packages/angular/src/generators/directive/__snapshots__/directive.spec.ts.snap +++ b/packages/angular/src/generators/directive/__snapshots__/directive.spec.ts.snap @@ -125,3 +125,27 @@ describe('TestDirective', () => { }); " `; + +exports[`directive generator should handle path with file extension 1`] = ` +"import { Directive } from '@angular/core'; + +@Directive({ + selector: '[test]', +}) +export class TestDirective { + constructor() {} +} +" +`; + +exports[`directive generator should handle path with file extension 2`] = ` +"import { TestDirective } from './test.directive'; + +describe('TestDirective', () => { + it('should create an instance', () => { + const directive = new TestDirective(); + expect(directive).toBeTruthy(); + }); +}); +" +`; diff --git a/packages/angular/src/generators/directive/directive.spec.ts b/packages/angular/src/generators/directive/directive.spec.ts index b980a596f33f1..1dab48dbc6998 100644 --- a/packages/angular/src/generators/directive/directive.spec.ts +++ b/packages/angular/src/generators/directive/directive.spec.ts @@ -35,6 +35,20 @@ describe('directive generator', () => { ).toMatchSnapshot(); }); + it('should handle path with file extension', async () => { + await generateDirectiveWithDefaultOptions(tree, { + path: 'test/src/app/test.directive.ts', + skipFormat: false, + }); + + expect( + tree.read('test/src/app/test.directive.ts', 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read('test/src/app/test.directive.spec.ts', 'utf-8') + ).toMatchSnapshot(); + }); + it('should not import the directive into an existing module', async () => { // ARRANGE addModule(tree); diff --git a/packages/angular/src/generators/directive/lib/normalize-options.ts b/packages/angular/src/generators/directive/lib/normalize-options.ts index bcd28adb7e62d..50baeb4833a1c 100644 --- a/packages/angular/src/generators/directive/lib/normalize-options.ts +++ b/packages/angular/src/generators/directive/lib/normalize-options.ts @@ -20,6 +20,8 @@ export async function normalizeOptions( name: options.name, path: options.path, suffix: 'directive', + allowedFileExtensions: ['ts'], + fileExtension: 'ts', }); const { className } = names(name); diff --git a/packages/angular/src/generators/directive/schema.json b/packages/angular/src/generators/directive/schema.json index 28d3e29bae9b8..3100479eeffcc 100644 --- a/packages/angular/src/generators/directive/schema.json +++ b/packages/angular/src/generators/directive/schema.json @@ -9,6 +9,10 @@ "examples": [ { "description": "Generate a directive with the exported symbol matching the file name. It results in the directive `FooDirective` at `mylib/src/lib/foo.directive.ts`", + "command": "nx g @nx/angular:directive mylib/src/lib/foo.directive.ts" + }, + { + "description": "Generate a directive without providing the file extension. It results in the directive `FooDirective` at `mylib/src/lib/foo.directive.ts`", "command": "nx g @nx/angular:directive mylib/src/lib/foo" }, { @@ -19,7 +23,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the directive without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the directive. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 diff --git a/packages/angular/src/generators/pipe/__snapshots__/pipe.spec.ts.snap b/packages/angular/src/generators/pipe/__snapshots__/pipe.spec.ts.snap index eeb3bcfea90ef..8fe79ad3e90fc 100644 --- a/packages/angular/src/generators/pipe/__snapshots__/pipe.spec.ts.snap +++ b/packages/angular/src/generators/pipe/__snapshots__/pipe.spec.ts.snap @@ -154,3 +154,29 @@ describe('TestPipe', () => { }); " `; + +exports[`pipe generator should handle path with file extension 1`] = ` +"import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'test', +}) +export class TestPipe implements PipeTransform { + transform(value: unknown, ...args: unknown[]): unknown { + return null; + } +} +" +`; + +exports[`pipe generator should handle path with file extension 2`] = ` +"import { TestPipe } from './test.pipe'; + +describe('TestPipe', () => { + it('create an instance', () => { + const pipe = new TestPipe(); + expect(pipe).toBeTruthy(); + }); +}); +" +`; diff --git a/packages/angular/src/generators/pipe/lib/normalize-options.ts b/packages/angular/src/generators/pipe/lib/normalize-options.ts index 6e2e86f2a4aaa..9ee2ca660d831 100644 --- a/packages/angular/src/generators/pipe/lib/normalize-options.ts +++ b/packages/angular/src/generators/pipe/lib/normalize-options.ts @@ -18,6 +18,8 @@ export async function normalizeOptions( name: options.name, path: options.path, suffix: 'pipe', + allowedFileExtensions: ['ts'], + fileExtension: 'ts', }); const { className } = names(name); diff --git a/packages/angular/src/generators/pipe/pipe.spec.ts b/packages/angular/src/generators/pipe/pipe.spec.ts index df3d6a066914c..a06524b165ed5 100644 --- a/packages/angular/src/generators/pipe/pipe.spec.ts +++ b/packages/angular/src/generators/pipe/pipe.spec.ts @@ -27,6 +27,18 @@ describe('pipe generator', () => { ).toMatchSnapshot(); }); + it('should handle path with file extension', async () => { + await generatePipeWithDefaultOptions(tree, { + path: 'test/src/app/test.pipe.ts', + skipFormat: false, + }); + + expect(tree.read('test/src/app/test.pipe.ts', 'utf-8')).toMatchSnapshot(); + expect( + tree.read('test/src/app/test.pipe.spec.ts', 'utf-8') + ).toMatchSnapshot(); + }); + it('should not import the pipe into an existing module', async () => { // ARRANGE addModule(tree); diff --git a/packages/angular/src/generators/pipe/schema.json b/packages/angular/src/generators/pipe/schema.json index 6698b4fa83eec..a3769b07c7e45 100644 --- a/packages/angular/src/generators/pipe/schema.json +++ b/packages/angular/src/generators/pipe/schema.json @@ -9,6 +9,10 @@ "examples": [ { "description": "Generate a pipe with the exported symbol matching the file name. It results in the pipe `FooPipe` at `mylib/src/lib/foo.pipe.ts`", + "command": "nx g @nx/angular:pipe mylib/src/lib/foo.pipe.ts" + }, + { + "description": "Generate a pipe without providing the file extension. It results in the pipe `FooPipe` at `mylib/src/lib/foo.pipe.ts`", "command": "nx g @nx/angular:pipe mylib/src/lib/foo" }, { @@ -19,7 +23,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the pipe without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the pipe. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 diff --git a/packages/angular/src/generators/scam-directive/lib/normalize-options.ts b/packages/angular/src/generators/scam-directive/lib/normalize-options.ts index 6eb23cd661eae..f8392cddd3174 100644 --- a/packages/angular/src/generators/scam-directive/lib/normalize-options.ts +++ b/packages/angular/src/generators/scam-directive/lib/normalize-options.ts @@ -18,6 +18,8 @@ export async function normalizeOptions( name: options.name, path: options.path, suffix: 'directive', + allowedFileExtensions: ['ts'], + fileExtension: 'ts', }); const { className } = names(name); diff --git a/packages/angular/src/generators/scam-directive/scam-directive.spec.ts b/packages/angular/src/generators/scam-directive/scam-directive.spec.ts index df35b11b43911..b2bcea3694807 100644 --- a/packages/angular/src/generators/scam-directive/scam-directive.spec.ts +++ b/packages/angular/src/generators/scam-directive/scam-directive.spec.ts @@ -84,6 +84,47 @@ describe('SCAM Directive Generator', () => { `); }); + it('should handle path with file extension', async () => { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + addProjectConfiguration(tree, 'app1', { + projectType: 'application', + sourceRoot: 'apps/app1/src', + root: 'apps/app1', + }); + + await scamDirectiveGenerator(tree, { + name: 'example', + path: 'apps/app1/src/app/example.directive.ts', + inlineScam: true, + skipFormat: true, + }); + + const directiveSource = tree.read( + 'apps/app1/src/app/example.directive.ts', + 'utf-8' + ); + expect(directiveSource).toMatchInlineSnapshot(` + "import { Directive, NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + + @Directive({ + selector: '[example]', + standalone: false + }) + export class ExampleDirective { + constructor() {} + } + + @NgModule({ + imports: [CommonModule], + declarations: [ExampleDirective], + exports: [ExampleDirective], + }) + export class ExampleDirectiveModule {} + " + `); + }); + it('should create the scam directive correctly and export it for a secondary entrypoint', async () => { // ARRANGE const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); diff --git a/packages/angular/src/generators/scam-directive/schema.json b/packages/angular/src/generators/scam-directive/schema.json index 79ad26713c7e9..a4de0a5cabc36 100644 --- a/packages/angular/src/generators/scam-directive/schema.json +++ b/packages/angular/src/generators/scam-directive/schema.json @@ -7,6 +7,10 @@ "examples": [ { "description": "Generate a directive with the exported symbol matching the file name. It results in the directive `FooDirective` at `mylib/src/lib/foo.directive.ts`", + "command": "nx g @nx/angular:scam-directive mylib/src/lib/foo.directive.ts" + }, + { + "description": "Generate a directive without providing the file extension. It results in the directive `FooDirective` at `mylib/src/lib/foo.directive.ts`", "command": "nx g @nx/angular:scam-directive mylib/src/lib/foo" }, { @@ -19,7 +23,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the SCAM directive without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the SCAM directive. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 diff --git a/packages/angular/src/generators/scam-pipe/lib/normalize-options.ts b/packages/angular/src/generators/scam-pipe/lib/normalize-options.ts index f8057e1ca8608..b1aaf04842a2a 100644 --- a/packages/angular/src/generators/scam-pipe/lib/normalize-options.ts +++ b/packages/angular/src/generators/scam-pipe/lib/normalize-options.ts @@ -18,6 +18,8 @@ export async function normalizeOptions( name: options.name, path: options.path, suffix: 'pipe', + allowedFileExtensions: ['ts'], + fileExtension: 'ts', }); const { className } = names(name); diff --git a/packages/angular/src/generators/scam-pipe/scam-pipe.spec.ts b/packages/angular/src/generators/scam-pipe/scam-pipe.spec.ts index 3d1caf6df5dbb..dcd5ae25d5132 100644 --- a/packages/angular/src/generators/scam-pipe/scam-pipe.spec.ts +++ b/packages/angular/src/generators/scam-pipe/scam-pipe.spec.ts @@ -86,6 +86,49 @@ describe('SCAM Pipe Generator', () => { `); }); + it('should handle path with file extension', async () => { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + addProjectConfiguration(tree, 'app1', { + projectType: 'application', + sourceRoot: 'apps/app1/src', + root: 'apps/app1', + }); + + await scamPipeGenerator(tree, { + name: 'example', + path: 'apps/app1/src/app/example/example.pipe.ts', + inlineScam: true, + skipFormat: true, + }); + + const pipeSource = tree.read( + 'apps/app1/src/app/example/example.pipe.ts', + 'utf-8' + ); + expect(pipeSource).toMatchInlineSnapshot(` + "import { Pipe, PipeTransform, NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + + @Pipe({ + name: 'example', + standalone: false + }) + export class ExamplePipe implements PipeTransform { + transform(value: unknown, ...args: unknown[]): unknown { + return null; + } + } + + @NgModule({ + imports: [CommonModule], + declarations: [ExamplePipe], + exports: [ExamplePipe], + }) + export class ExamplePipeModule {} + " + `); + }); + it('should create the scam pipe correctly and export it for a secondary entrypoint', async () => { // ARRANGE const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); diff --git a/packages/angular/src/generators/scam-pipe/schema.json b/packages/angular/src/generators/scam-pipe/schema.json index 90e0982352cf4..7f3ce2b740ae4 100644 --- a/packages/angular/src/generators/scam-pipe/schema.json +++ b/packages/angular/src/generators/scam-pipe/schema.json @@ -7,6 +7,10 @@ "examples": [ { "description": "Generate a pipe with the exported symbol matching the file name. It results in the pipe `FooPipe` at `mylib/src/lib/foo.pipe.ts`", + "command": "nx g @nx/angular:scam-pipe mylib/src/lib/foo.pipe.ts" + }, + { + "description": "Generate a pipe without providing the file extension. It results in the pipe `FooPipe` at `mylib/src/lib/foo.pipe.ts`", "command": "nx g @nx/angular:scam-pipe mylib/src/lib/foo" }, { @@ -19,7 +23,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the SCAM pipe without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the SCAM pipe. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 diff --git a/packages/angular/src/generators/scam/lib/normalize-options.ts b/packages/angular/src/generators/scam/lib/normalize-options.ts index 99ef237a18801..897011267758a 100644 --- a/packages/angular/src/generators/scam/lib/normalize-options.ts +++ b/packages/angular/src/generators/scam/lib/normalize-options.ts @@ -19,6 +19,8 @@ export async function normalizeOptions( name: options.name, path: options.path, suffix: options.type ?? 'component', + allowedFileExtensions: ['ts'], + fileExtension: 'ts', }); const { className } = names(name); diff --git a/packages/angular/src/generators/scam/scam.spec.ts b/packages/angular/src/generators/scam/scam.spec.ts index ce4268a634eed..4f0fe2205f7b0 100644 --- a/packages/angular/src/generators/scam/scam.spec.ts +++ b/packages/angular/src/generators/scam/scam.spec.ts @@ -84,6 +84,47 @@ describe('SCAM Generator', () => { `); }); + it('should handle path with file extension', async () => { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + addProjectConfiguration(tree, 'app1', { + projectType: 'application', + sourceRoot: 'apps/app1/src', + root: 'apps/app1', + }); + + await scamGenerator(tree, { + name: 'example', + path: 'apps/app1/src/app/example/example.component.ts', + inlineScam: true, + skipFormat: true, + }); + + const componentSource = tree.read( + 'apps/app1/src/app/example/example.component.ts', + 'utf-8' + ); + expect(componentSource).toMatchInlineSnapshot(` + "import { Component, NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + + @Component({ + selector: 'example', + standalone: false, + templateUrl: './example.component.html', + styleUrl: './example.component.css' + }) + export class ExampleComponent {} + + @NgModule({ + imports: [CommonModule], + declarations: [ExampleComponent], + exports: [ExampleComponent], + }) + export class ExampleComponentModule {} + " + `); + }); + it('should create the scam correctly and export it for a secondary entrypoint', async () => { // ARRANGE const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); diff --git a/packages/angular/src/generators/scam/schema.json b/packages/angular/src/generators/scam/schema.json index 2d005a07bac00..22c1e9621359f 100644 --- a/packages/angular/src/generators/scam/schema.json +++ b/packages/angular/src/generators/scam/schema.json @@ -7,6 +7,10 @@ "examples": [ { "description": "Generate a component with the exported symbol matching the file name. It results in the component `FooComponent` at `mylib/src/lib/foo.component.ts`", + "command": "nx g @nx/angular:scam mylib/src/lib/foo.component.ts" + }, + { + "description": "Generate a component without providing the file extension. It results in the component `FooComponent` at `mylib/src/lib/foo.component.ts`", "command": "nx g @nx/angular:scam mylib/src/lib/foo" }, { @@ -19,7 +23,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the SCAM without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the SCAM. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 diff --git a/packages/devkit/src/generators/artifact-name-and-directory-utils.spec.ts b/packages/devkit/src/generators/artifact-name-and-directory-utils.spec.ts index 1589e65ec5f25..44da0ac08a1c5 100644 --- a/packages/devkit/src/generators/artifact-name-and-directory-utils.spec.ts +++ b/packages/devkit/src/generators/artifact-name-and-directory-utils.spec.ts @@ -26,7 +26,7 @@ describe('determineArtifactNameAndDirectoryOptions', () => { originalInitCwd = process.env.INIT_CWD; }); - it('should throw an error when the resolver directory is not under any project root', async () => { + it('should throw an error when the resolved directory is not under any project root', async () => { addProjectConfiguration(tree, 'app1', { root: 'apps/app1', projectType: 'application', @@ -44,7 +44,7 @@ describe('determineArtifactNameAndDirectoryOptions', () => { restoreCwd(); }); - it('should return options as provided when there is a project at the cwd', async () => { + it('should return the normalized options when there is a project at the cwd', async () => { addProjectConfiguration(tree, 'app1', { root: 'apps/app1', projectType: 'application', @@ -52,7 +52,7 @@ describe('determineArtifactNameAndDirectoryOptions', () => { setCwd('apps/app1'); const result = await determineArtifactNameAndDirectoryOptions(tree, { - path: 'apps/app1/myComponent', + path: 'myComponent', }); expect(result).toStrictEqual({ @@ -60,13 +60,15 @@ describe('determineArtifactNameAndDirectoryOptions', () => { directory: 'apps/app1', fileName: 'myComponent', filePath: 'apps/app1/myComponent.ts', + fileExtension: 'ts', + fileExtensionType: 'ts', project: 'app1', }); restoreCwd(); }); - it('should not duplicate the cwd when the provided directory starts with the cwd and format is "as-provided"', async () => { + it('should not duplicate the cwd when the provided directory starts with the cwd', async () => { addProjectConfiguration(tree, 'app1', { root: 'apps/app1', projectType: 'application', @@ -82,20 +84,22 @@ describe('determineArtifactNameAndDirectoryOptions', () => { directory: 'apps/app1', fileName: 'myComponent', filePath: 'apps/app1/myComponent.ts', + fileExtension: 'ts', + fileExtensionType: 'ts', project: 'app1', }); restoreCwd(); }); - it('should return the options as provided when directory is provided', async () => { + it(`should handle window's style paths correctly`, async () => { addProjectConfiguration(tree, 'app1', { root: 'apps/app1', projectType: 'application', }); const result = await determineArtifactNameAndDirectoryOptions(tree, { - path: 'apps/app1/myComponent', + path: 'apps\\app1\\myComponent', }); expect(result).toStrictEqual({ @@ -103,105 +107,184 @@ describe('determineArtifactNameAndDirectoryOptions', () => { directory: 'apps/app1', fileName: 'myComponent', filePath: 'apps/app1/myComponent.ts', + fileExtension: 'ts', + fileExtensionType: 'ts', project: 'app1', }); }); - it(`should handle window's style paths correctly`, async () => { + it('should support receiving a suffix', async () => { addProjectConfiguration(tree, 'app1', { root: 'apps/app1', projectType: 'application', }); const result = await determineArtifactNameAndDirectoryOptions(tree, { - path: 'apps\\app1\\myComponent', + suffix: 'component', + path: 'apps/app1/myComponent', }); expect(result).toStrictEqual({ artifactName: 'myComponent', directory: 'apps/app1', - fileName: 'myComponent', - filePath: 'apps/app1/myComponent.ts', + fileName: 'myComponent.component', + filePath: 'apps/app1/myComponent.component.ts', + fileExtension: 'ts', + fileExtensionType: 'ts', project: 'app1', }); }); - it('should support receiving a path as the name', async () => { + it('should support receiving the full file path including the file extension', async () => { addProjectConfiguration(tree, 'app1', { root: 'apps/app1', projectType: 'application', }); const result = await determineArtifactNameAndDirectoryOptions(tree, { - path: 'apps/app1/foo/bar/myComponent', + path: 'apps/app1/myComponent.ts', }); expect(result).toStrictEqual({ artifactName: 'myComponent', - directory: 'apps/app1/foo/bar', + directory: 'apps/app1', fileName: 'myComponent', - filePath: 'apps/app1/foo/bar/myComponent.ts', + filePath: 'apps/app1/myComponent.ts', + fileExtension: 'ts', + fileExtensionType: 'ts', project: 'app1', }); }); - it('should support receiving a suffix', async () => { + it('should ignore specified suffix when receiving the full file path including the file extension', async () => { addProjectConfiguration(tree, 'app1', { root: 'apps/app1', projectType: 'application', }); const result = await determineArtifactNameAndDirectoryOptions(tree, { + path: 'apps/app1/myComponent.ts', suffix: 'component', - path: 'apps/app1/myComponent', }); expect(result).toStrictEqual({ artifactName: 'myComponent', directory: 'apps/app1', - fileName: 'myComponent.component', - filePath: 'apps/app1/myComponent.component.ts', + fileName: 'myComponent', + filePath: 'apps/app1/myComponent.ts', + fileExtension: 'ts', + fileExtensionType: 'ts', project: 'app1', }); }); - it('should support receiving a fileName', async () => { + it('should support receiving a different file extension', async () => { addProjectConfiguration(tree, 'app1', { root: 'apps/app1', projectType: 'application', }); const result = await determineArtifactNameAndDirectoryOptions(tree, { - fileName: 'myComponent.component', + fileExtension: 'tsx', path: 'apps/app1/myComponent', }); expect(result).toStrictEqual({ artifactName: 'myComponent', directory: 'apps/app1', - fileName: 'myComponent.component', - filePath: 'apps/app1/myComponent.component.ts', + fileName: 'myComponent', + filePath: 'apps/app1/myComponent.tsx', + fileExtension: 'tsx', + fileExtensionType: 'ts', project: 'app1', }); }); - it('should support receiving a different file extension', async () => { + it('should support receiving a file path with a non-default file extension', async () => { addProjectConfiguration(tree, 'app1', { root: 'apps/app1', projectType: 'application', }); const result = await determineArtifactNameAndDirectoryOptions(tree, { - fileExtension: 'tsx', - path: 'apps/app1/myComponent', + path: 'apps/app1/myComponent.astro', + allowedFileExtensions: ['astro'], }); expect(result).toStrictEqual({ artifactName: 'myComponent', directory: 'apps/app1', fileName: 'myComponent', - filePath: 'apps/app1/myComponent.tsx', + filePath: 'apps/app1/myComponent.astro', + fileExtension: 'astro', + fileExtensionType: 'other', project: 'app1', }); }); + + it('should throw an error when the file extension is not supported', async () => { + addProjectConfiguration(tree, 'app1', { + root: 'apps/app1', + projectType: 'application', + }); + + await expect( + determineArtifactNameAndDirectoryOptions(tree, { + path: 'apps/app1/myComponent.ts', + allowedFileExtensions: ['jsx', 'tsx'], + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "The provided file path has an extension (.ts) that is not supported by this generator. + The supported extensions are: .jsx, .tsx." + `); + }); + + it('should throw an error when having a TypeScript file extension and the --js option is used', async () => { + addProjectConfiguration(tree, 'app1', { + root: 'apps/app1', + projectType: 'application', + }); + + await expect( + determineArtifactNameAndDirectoryOptions(tree, { + path: 'apps/app1/myComponent.tsx', + js: true, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"The provided file path has an extension (.tsx) that conflicts with the provided "--js" option."` + ); + }); + + it('should throw an error when having a JavaScript file extension and the --js=false option is used', async () => { + addProjectConfiguration(tree, 'app1', { + root: 'apps/app1', + projectType: 'application', + }); + + await expect( + determineArtifactNameAndDirectoryOptions(tree, { + path: 'apps/app1/myComponent.jsx', + js: false, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"The provided file path has an extension (.jsx) that conflicts with the provided "--js" option."` + ); + }); + + it('should support customizing the --js option name', async () => { + addProjectConfiguration(tree, 'app1', { + root: 'apps/app1', + projectType: 'application', + }); + + await expect( + determineArtifactNameAndDirectoryOptions(tree, { + path: 'apps/app1/myComponent.tsx', + js: true, + jsOptionName: 'language', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"The provided file path has an extension (.tsx) that conflicts with the provided "--language" option."` + ); + }); }); diff --git a/packages/devkit/src/generators/artifact-name-and-directory-utils.ts b/packages/devkit/src/generators/artifact-name-and-directory-utils.ts index 59ad8c2185bee..e0f2b308bc1b7 100644 --- a/packages/devkit/src/generators/artifact-name-and-directory-utils.ts +++ b/packages/devkit/src/generators/artifact-name-and-directory-utils.ts @@ -12,14 +12,32 @@ import { } from 'nx/src/devkit-internals'; import { join, relative } from 'path'; +const DEFAULT_ALLOWED_JS_FILE_EXTENSIONS = ['js', 'cjs', 'mjs', 'jsx']; +const DEFAULT_ALLOWED_TS_FILE_EXTENSIONS = ['ts', 'cts', 'mts', 'tsx']; +const DEFAULT_ALLOWED_FILE_EXTENSIONS = [ + ...DEFAULT_ALLOWED_JS_FILE_EXTENSIONS, + ...DEFAULT_ALLOWED_TS_FILE_EXTENSIONS, + 'vue', +]; + export type ArtifactGenerationOptions = { path: string; name?: string; - fileExtension?: 'js' | 'jsx' | 'ts' | 'tsx' | 'vue'; - fileName?: string; + fileExtension?: string; suffix?: string; + allowedFileExtensions?: string[]; + + /** + * @deprecated Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21. + */ + js?: boolean; + /** + * @deprecated Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21. + */ + jsOptionName?: string; }; +export type FileExtensionType = 'js' | 'ts' | 'other'; export type NameAndDirectoryOptions = { /** * Normalized artifact name. @@ -33,6 +51,14 @@ export type NameAndDirectoryOptions = { * Normalized file name of the artifact without the extension. */ fileName: string; + /** + * Normalized file extension. + */ + fileExtension: string; + /** + * Normalized file extension type. + */ + fileExtensionType: FileExtensionType; /** * Normalized full file path of the artifact. */ @@ -60,11 +86,10 @@ export async function determineArtifactNameAndDirectoryOptions( function getNameAndDirectoryOptions( tree: Tree, options: ArtifactGenerationOptions -) { +): NameAndDirectoryOptions { const path = options.path ? normalizePath(options.path.replace(/^\.?\//, '')) : undefined; - const fileExtension = options.fileExtension ?? 'ts'; let { name: extractedName, directory } = extractNameAndDirectoryFromPath(path); const relativeCwd = getRelativeCwd(); @@ -75,17 +100,43 @@ function getNameAndDirectoryOptions( } const project = findProjectFromPath(tree, directory); - const name = - options.fileName ?? - (options.suffix ? `${extractedName}.${options.suffix}` : extractedName); - const filePath = joinPathFragments(directory, `${name}.${fileExtension}`); + + let fileName = extractedName; + let fileExtension: string = options.fileExtension ?? 'ts'; + + const allowedFileExtensions = + options.allowedFileExtensions ?? DEFAULT_ALLOWED_FILE_EXTENSIONS; + const fileExtensionRegex = new RegExp( + `\\.(${allowedFileExtensions.join('|')})$` + ); + const fileExtensionMatch = fileName.match(fileExtensionRegex); + + if (fileExtensionMatch) { + fileExtension = fileExtensionMatch[1]; + fileName = fileName.replace(fileExtensionRegex, ''); + extractedName = fileName; + } else if (options.suffix) { + fileName = `${fileName}.${options.suffix}`; + } + + const filePath = joinPathFragments(directory, `${fileName}.${fileExtension}`); + const fileExtensionType = getFileExtensionType(fileExtension); + + validateFileExtension( + fileExtension, + allowedFileExtensions, + options.js, + options.jsOptionName + ); return { artifactName: options.name ?? extractedName, - directory: directory, - fileName: name, - filePath: filePath, - project: project, + directory, + fileName, + fileExtension, + fileExtensionType, + filePath, + project, }; } @@ -145,3 +196,50 @@ function extractNameAndDirectoryFromPath(path: string): { return { name, directory }; } + +function getFileExtensionType(fileExtension: string): FileExtensionType { + if (DEFAULT_ALLOWED_JS_FILE_EXTENSIONS.includes(fileExtension)) { + return 'js'; + } + + if (DEFAULT_ALLOWED_TS_FILE_EXTENSIONS.includes(fileExtension)) { + return 'ts'; + } + + return 'other'; +} + +function validateFileExtension( + fileExtension: string, + allowedFileExtensions: string[], + js: boolean | undefined, + jsOptionName: string | undefined +): FileExtensionType { + const fileExtensionType = getFileExtensionType(fileExtension); + + if (!allowedFileExtensions.includes(fileExtension)) { + throw new Error( + `The provided file path has an extension (.${fileExtension}) that is not supported by this generator. +The supported extensions are: ${allowedFileExtensions + .map((ext) => `.${ext}`) + .join(', ')}.` + ); + } + + if (js !== undefined) { + jsOptionName = jsOptionName ?? 'js'; + + if (js && fileExtensionType === 'ts') { + throw new Error( + `The provided file path has an extension (.${fileExtension}) that conflicts with the provided "--${jsOptionName}" option.` + ); + } + if (!js && fileExtensionType === 'js') { + throw new Error( + `The provided file path has an extension (.${fileExtension}) that conflicts with the provided "--${jsOptionName}" option.` + ); + } + } + + return fileExtensionType; +} diff --git a/packages/expo/src/generators/component/component.spec.ts b/packages/expo/src/generators/component/component.spec.ts index bb617cdfadd94..799b5ddf5b0c6 100644 --- a/packages/expo/src/generators/component/component.spec.ts +++ b/packages/expo/src/generators/component/component.spec.ts @@ -59,6 +59,16 @@ describe('component', () => { expect(appTree.exists('my-lib/src/lib/hello/hello.spec.tsx')).toBeTruthy(); }); + it('should handle path with file extension', async () => { + await expoComponentGenerator(appTree, { + ...defaultSchema, + path: 'my-lib/src/lib/hello/hello.tsx', + }); + + expect(appTree.exists('my-lib/src/lib/hello/hello.tsx')).toBeTruthy(); + expect(appTree.exists('my-lib/src/lib/hello/hello.spec.tsx')).toBeTruthy(); + }); + it('should generate files for an app', async () => { await expoComponentGenerator(appTree, { ...defaultSchema, diff --git a/packages/expo/src/generators/component/component.ts b/packages/expo/src/generators/component/component.ts index bdcfba96c65dc..c0e3f391b8250 100644 --- a/packages/expo/src/generators/component/component.ts +++ b/packages/expo/src/generators/component/component.ts @@ -6,7 +6,6 @@ import { generateFiles, getProjects, joinPathFragments, - toJS, Tree, } from '@nx/devkit'; import { NormalizedSchema, normalizeOptions } from './lib/normalize-options'; @@ -25,25 +24,23 @@ export async function expoComponentGenerator(host: Tree, schema: Schema) { } function createComponentFiles(host: Tree, options: NormalizedSchema) { - generateFiles(host, join(__dirname, './files'), options.directory, { - ...options, - tmpl: '', - }); - - for (const c of host.listChanges()) { - let deleteFile = false; - - if (options.skipTests && /.*spec.tsx/.test(c.path)) { - deleteFile = true; + generateFiles( + host, + join(__dirname, './files', options.fileExtensionType), + options.directory, + { + ...options, + ext: options.fileExtension, } + ); - if (deleteFile) { - host.delete(c.path); - } - } - - if (options.js) { - toJS(host); + if (options.skipTests) { + host.delete( + joinPathFragments( + options.directory, + `${options.fileName}.spec.${options.fileExtension}` + ) + ); } } @@ -55,26 +52,29 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) { if (options.export && !isApp) { const indexFilePath = joinPathFragments( options.projectSourceRoot, - options.js ? 'index.js' : 'index.ts' + options.fileExtensionType === 'js' ? 'index.js' : 'index.ts' ); - const indexSource = host.read(indexFilePath, 'utf-8'); - if (indexSource !== null) { - const indexSourceFile = ts.createSourceFile( - indexFilePath, - indexSource, - ts.ScriptTarget.Latest, - true - ); - const relativePathFromIndex = getRelativeImportToFile( - indexFilePath, - options.filePath - ); - const changes = applyChangesToString( - indexSource, - addImport(indexSourceFile, `export * from '${relativePathFromIndex}';`) - ); - host.write(indexFilePath, changes); + + if (!host.exists(indexFilePath)) { + return; } + + const indexSource = host.read(indexFilePath, 'utf-8'); + const indexSourceFile = ts.createSourceFile( + indexFilePath, + indexSource, + ts.ScriptTarget.Latest, + true + ); + const relativePathFromIndex = getRelativeImportToFile( + indexFilePath, + options.filePath + ); + const changes = applyChangesToString( + indexSource, + addImport(indexSourceFile, `export * from '${relativePathFromIndex}';`) + ); + host.write(indexFilePath, changes); } } diff --git a/packages/expo/src/generators/component/files/js/__fileName__.__ext__ b/packages/expo/src/generators/component/files/js/__fileName__.__ext__ new file mode 100644 index 0000000000000..37f4f213c2a1f --- /dev/null +++ b/packages/expo/src/generators/component/files/js/__fileName__.__ext__ @@ -0,0 +1,28 @@ +<%_ if (classComponent) { _%> +import { Component } from 'react'; +<%_ } else { _%> +import React from 'react'; +<%_ } _%> +import { View, Text } from 'react-native'; + +<%_ if (classComponent) { _%> +export class <%= className %> extends Component { + render() { + return ( + + Welcome to <%= name %>! + + ); + } +} +<%_ } else { _%> +export function <%= className %>(props) { + return ( + + Welcome to <%= name %>! + + ); +} +<%_ } _%> + +export default <%= className %>; diff --git a/packages/expo/src/generators/component/files/js/__fileName__.spec.__ext__ b/packages/expo/src/generators/component/files/js/__fileName__.spec.__ext__ new file mode 100644 index 0000000000000..88470ea7e2855 --- /dev/null +++ b/packages/expo/src/generators/component/files/js/__fileName__.spec.__ext__ @@ -0,0 +1,10 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import <%= className %> from './<%= fileName %>'; + +describe('<%= className %>', () => { + it('should render successfully', () => { + const { root } = render(< <%= className %> />); + expect(root).toBeTruthy(); + }); +}); diff --git a/packages/expo/src/generators/component/files/__fileName__.tsx.template b/packages/expo/src/generators/component/files/ts/__fileName__.__ext__ similarity index 82% rename from packages/expo/src/generators/component/files/__fileName__.tsx.template rename to packages/expo/src/generators/component/files/ts/__fileName__.__ext__ index f67db2c698ff2..b66220b95ddf3 100644 --- a/packages/expo/src/generators/component/files/__fileName__.tsx.template +++ b/packages/expo/src/generators/component/files/ts/__fileName__.__ext__ @@ -1,15 +1,15 @@ -<% if (classComponent) { %> +<%_ if (classComponent) { _%> import { Component } from 'react'; -<% } else { %> +<%_ } else { _%> import React from 'react'; -<% } %> +<%_ } _%> import { View, Text } from 'react-native'; /* eslint-disable-next-line */ export interface <%= className %>Props { } -<% if (classComponent) { %> +<%_ if (classComponent) { _%> export class <%= className %> extends Component<<%= className %>Props> { render() { return ( @@ -19,7 +19,7 @@ export class <%= className %> extends Component<<%= className %>Props> { ); } } -<% } else { %> +<%_ } else { _%> export function <%= className %>(props: <%= className %>Props) { return ( @@ -27,6 +27,6 @@ export function <%= className %>(props: <%= className %>Props) { ); } -<% } %> +<%_ } _%> export default <%= className %>; diff --git a/packages/expo/src/generators/component/files/ts/__fileName__.spec.__ext__ b/packages/expo/src/generators/component/files/ts/__fileName__.spec.__ext__ new file mode 100644 index 0000000000000..88470ea7e2855 --- /dev/null +++ b/packages/expo/src/generators/component/files/ts/__fileName__.spec.__ext__ @@ -0,0 +1,10 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import <%= className %> from './<%= fileName %>'; + +describe('<%= className %>', () => { + it('should render successfully', () => { + const { root } = render(< <%= className %> />); + expect(root).toBeTruthy(); + }); +}); diff --git a/packages/expo/src/generators/component/lib/normalize-options.ts b/packages/expo/src/generators/component/lib/normalize-options.ts index 25ee293b7b75b..c7268b731b05d 100644 --- a/packages/expo/src/generators/component/lib/normalize-options.ts +++ b/packages/expo/src/generators/component/lib/normalize-options.ts @@ -1,11 +1,16 @@ import { getProjects, logger, names, Tree } from '@nx/devkit'; +import { + determineArtifactNameAndDirectoryOptions, + type FileExtensionType, +} from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { Schema } from '../schema'; -import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; -export interface NormalizedSchema extends Schema { +export interface NormalizedSchema extends Omit { directory: string; projectSourceRoot: string; fileName: string; + fileExtension: string; + fileExtensionType: FileExtensionType; className: string; filePath: string; projectName: string; @@ -18,19 +23,21 @@ export async function normalizeOptions( const { artifactName: name, fileName, + fileExtension, + fileExtensionType, filePath, directory, project: projectName, } = await determineArtifactNameAndDirectoryOptions(host, { name: options.name, path: options.path, - fileExtension: 'tsx', + allowedFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + fileExtension: options.js ? 'js' : 'tsx', + js: options.js, }); - const project = getProjects(host).get(projectName); - const { className } = names(name); - + const project = getProjects(host).get(projectName); const { sourceRoot: projectSourceRoot, projectType } = project; if (options.export && projectType === 'application') { @@ -47,6 +54,8 @@ export async function normalizeOptions( directory, className, fileName, + fileExtension, + fileExtensionType, filePath, projectSourceRoot, projectName, diff --git a/packages/expo/src/generators/component/schema.d.ts b/packages/expo/src/generators/component/schema.d.ts index 09bf7caea01f5..1c71bd9768fa4 100644 --- a/packages/expo/src/generators/component/schema.d.ts +++ b/packages/expo/src/generators/component/schema.d.ts @@ -4,9 +4,13 @@ export interface Schema { path: string; name?: string; - skipFormat: boolean; // default is false - skipTests: boolean; // default is false - export: boolean; // default is false - classComponent: boolean; // default is false - js: boolean; // default is false + skipFormat?: boolean; + skipTests?: boolean; + export?: boolean; + classComponent?: boolean; + + /** + * @deprecated Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21. + */ + js?: boolean; } diff --git a/packages/expo/src/generators/component/schema.json b/packages/expo/src/generators/component/schema.json index 4d9ae633828fd..f0cb7ac7144b2 100644 --- a/packages/expo/src/generators/component/schema.json +++ b/packages/expo/src/generators/component/schema.json @@ -7,11 +7,15 @@ "examples": [ { "description": "Generate a component with the exported symbol matching the file name. It results in the component `Foo` at `mylib/src/foo.tsx`", - "command": "nx g @nx/expo:component mylib/src/foo" + "command": "nx g @nx/expo:component mylib/src/foo.tsx" }, { "description": "Generate a component with the exported symbol different from the file name. It results in the component `Custom` at `mylib/src/foo.tsx`", - "command": "nx g @nx/expo:component mylib/src/foo --name=custom" + "command": "nx g @nx/expo:component mylib/src/foo.tsx --name=custom" + }, + { + "description": "Generate a component without the providing the file extension. It results in the component `Foo` at `mylib/src/foo.tsx`", + "command": "nx g @nx/expo:component mylib/src/foo" }, { "description": "Generate a class component at `mylib/src/foo.tsx`", @@ -21,7 +25,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 @@ -35,7 +39,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipFormat": { "description": "Skip formatting files.", diff --git a/packages/nest/src/generators/class/schema.json b/packages/nest/src/generators/class/schema.json index 55188ad976881..ed99143399436 100644 --- a/packages/nest/src/generators/class/schema.json +++ b/packages/nest/src/generators/class/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the class `Foo` at `myapp/src/app/foo.ts`", + "command": "nx g @nx/nest:class myapp/src/app/foo.ts" + }, + { + "description": "Generate the class without providing the file extension. It results in the class `Foo` at `myapp/src/app/foo.ts`", "command": "nx g @nx/nest:class myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the class without the file extension. Relative to the current working directory.", + "description": "The file path to the class. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/controller/controller.ts b/packages/nest/src/generators/controller/controller.ts index 65fc1cc16faea..0f475ac43dc88 100644 --- a/packages/nest/src/generators/controller/controller.ts +++ b/packages/nest/src/generators/controller/controller.ts @@ -31,7 +31,9 @@ async function normalizeControllerOptions( tree: Tree, options: ControllerGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'controller', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/controller/schema.json b/packages/nest/src/generators/controller/schema.json index c583a1627f42c..cf42fb94f5d3c 100644 --- a/packages/nest/src/generators/controller/schema.json +++ b/packages/nest/src/generators/controller/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the controller `FooController` at `myapp/src/app/foo.controller.ts`", + "command": "nx g @nx/nest:controller myapp/src/app/foo.controller.ts" + }, + { + "description": "Generate the controller without providing the file extension. It results in the controller `FooController` at `myapp/src/app/foo.controller.ts`", "command": "nx g @nx/nest:controller myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the controller without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the controller. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/decorator/decorator.ts b/packages/nest/src/generators/decorator/decorator.ts index 7126705a0f82f..7797fa7587e19 100644 --- a/packages/nest/src/generators/decorator/decorator.ts +++ b/packages/nest/src/generators/decorator/decorator.ts @@ -22,7 +22,9 @@ async function normalizeDecoratorOptions( tree: Tree, options: DecoratorGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'decorator', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/decorator/schema.json b/packages/nest/src/generators/decorator/schema.json index bd481761fb0fc..9e53cb3538d69 100644 --- a/packages/nest/src/generators/decorator/schema.json +++ b/packages/nest/src/generators/decorator/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the decorator `Foo` at `myapp/src/app/foo.decorator.ts`", + "command": "nx g @nx/nest:decorator myapp/src/app/foo.decorator.ts" + }, + { + "description": "Generate the decorator without providing the file extension. It results in the decorator `Foo` at `myapp/src/app/foo.decorator.ts`", "command": "nx g @nx/nest:decorator myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the decorator without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the decorator. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/filter/filter.ts b/packages/nest/src/generators/filter/filter.ts index 8548e927a9548..c511a1840f645 100644 --- a/packages/nest/src/generators/filter/filter.ts +++ b/packages/nest/src/generators/filter/filter.ts @@ -28,7 +28,9 @@ async function normalizeFilterOptions( tree: Tree, options: FilterGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'filter', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/filter/schema.json b/packages/nest/src/generators/filter/schema.json index 9ed282afb9d51..6f69c0aef8f99 100644 --- a/packages/nest/src/generators/filter/schema.json +++ b/packages/nest/src/generators/filter/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the filter `FooFilter` at `myapp/src/app/foo.filter.ts`", + "command": "nx g @nx/nest:filter myapp/src/app/foo.filter.ts" + }, + { + "description": "Generate the filter without providing the file extension. It results in the filter `FooFilter` at `myapp/src/app/foo.filter.ts`", "command": "nx g @nx/nest:filter myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the filter without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the filter. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/gateway/gateway.ts b/packages/nest/src/generators/gateway/gateway.ts index 59992dc1efae1..57f836598a580 100644 --- a/packages/nest/src/generators/gateway/gateway.ts +++ b/packages/nest/src/generators/gateway/gateway.ts @@ -28,7 +28,9 @@ async function normalizeGatewayOptions( tree: Tree, options: GatewayGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'gateway', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/gateway/schema.json b/packages/nest/src/generators/gateway/schema.json index 16550c2fb98a4..cc9b7c0f86572 100644 --- a/packages/nest/src/generators/gateway/schema.json +++ b/packages/nest/src/generators/gateway/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the gateway `FooGateway` at `myapp/src/app/foo.gateway.ts`", + "command": "nx g @nx/nest:gateway myapp/src/app/foo.gateway.ts" + }, + { + "description": "Generate the gateway without providing the file extension. It results in the gateway `FooGateway` at `myapp/src/app/foo.gateway.ts`", "command": "nx g @nx/nest:gateway myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the gateway without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the gateway. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/guard/guard.ts b/packages/nest/src/generators/guard/guard.ts index b4cc38bebc089..e38194fc64932 100644 --- a/packages/nest/src/generators/guard/guard.ts +++ b/packages/nest/src/generators/guard/guard.ts @@ -28,7 +28,9 @@ async function normalizeGuardOptions( tree: Tree, options: GuardGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'guard', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/guard/schema.json b/packages/nest/src/generators/guard/schema.json index 388f8246e3ca6..44a32f3a7394a 100644 --- a/packages/nest/src/generators/guard/schema.json +++ b/packages/nest/src/generators/guard/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the guard `FooGuard` at `myapp/src/app/foo.guard.ts`", + "command": "nx g @nx/nest:guard myapp/src/app/foo.guard.ts" + }, + { + "description": "Generate the guard without providing the file extension. It results in the guard `FooGuard` at `myapp/src/app/foo.guard.ts`", "command": "nx g @nx/nest:guard myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the guard without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the guard. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/interceptor/interceptor.ts b/packages/nest/src/generators/interceptor/interceptor.ts index 67d3ad7584be5..3bd5834e55cf3 100644 --- a/packages/nest/src/generators/interceptor/interceptor.ts +++ b/packages/nest/src/generators/interceptor/interceptor.ts @@ -28,7 +28,9 @@ async function normalizeInterceptorOptions( tree: Tree, options: InterceptorGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'interceptor', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/interceptor/schema.json b/packages/nest/src/generators/interceptor/schema.json index 21604cc5fbe63..23e3855820cfd 100644 --- a/packages/nest/src/generators/interceptor/schema.json +++ b/packages/nest/src/generators/interceptor/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the interceptor `FooInterceptor` at `myapp/src/app/foo.interceptor.ts`", + "command": "nx g @nx/nest:interceptor myapp/src/app/foo.interceptor.ts" + }, + { + "description": "Generate the interceptor without providing the file extension. It results in the interceptor `FooInterceptor` at `myapp/src/app/foo.interceptor.ts`", "command": "nx g @nx/nest:interceptor myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the interceptor without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the interceptor. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/interface/interface.ts b/packages/nest/src/generators/interface/interface.ts index 4ed3cfa16942b..993fcc4b02762 100644 --- a/packages/nest/src/generators/interface/interface.ts +++ b/packages/nest/src/generators/interface/interface.ts @@ -8,7 +8,11 @@ export async function interfaceGenerator( tree: Tree, rawOptions: InterfaceGeneratorOptions ): Promise { - const options = await normalizeOptions(tree, rawOptions); + const options = await normalizeOptions(tree, rawOptions, { + allowedFileExtensions: ['ts'], + skipLanguageOption: true, + suffix: 'interface', + }); return runNestSchematic(tree, 'interface', options); } diff --git a/packages/nest/src/generators/interface/schema.json b/packages/nest/src/generators/interface/schema.json index c8bfd3fe6ff2a..9394a83530515 100644 --- a/packages/nest/src/generators/interface/schema.json +++ b/packages/nest/src/generators/interface/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the interface `Foo` at `myapp/src/app/foo.interface.ts`", + "command": "nx g @nx/nest:interface myapp/src/app/foo.interface.ts" + }, + { + "description": "Generate the interface without providing the file extension. It results in the interface `Foo` at `myapp/src/app/foo.interface.ts`", "command": "nx g @nx/nest:interface myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the interface without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the interface. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/middleware/middleware.ts b/packages/nest/src/generators/middleware/middleware.ts index b7f3e3bbed069..3392e171239d2 100644 --- a/packages/nest/src/generators/middleware/middleware.ts +++ b/packages/nest/src/generators/middleware/middleware.ts @@ -28,7 +28,9 @@ async function normalizeMiddlewareOptions( tree: Tree, options: MiddlewareGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'middleware', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/middleware/schema.json b/packages/nest/src/generators/middleware/schema.json index fba06ed958b89..11a6053877b64 100644 --- a/packages/nest/src/generators/middleware/schema.json +++ b/packages/nest/src/generators/middleware/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the middleware `FooMiddleware` at `myapp/src/app/foo.middleware.ts`", + "command": "nx g @nx/nest:middleware myapp/src/app/foo.middleware.ts" + }, + { + "description": "Generate the middleware without providing the file extension. It results in the middleware `FooMiddleware` at `myapp/src/app/foo.middleware.ts`", "command": "nx g @nx/nest:middleware myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the middleware without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the middleware. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/module/module.ts b/packages/nest/src/generators/module/module.ts index d7036b1b924f9..56f813fea4f2f 100644 --- a/packages/nest/src/generators/module/module.ts +++ b/packages/nest/src/generators/module/module.ts @@ -25,7 +25,9 @@ async function normalizeModuleOptions( tree: Tree, options: ModuleGeneratorOptions ): Promise { - const normalizedOption = await normalizeOptions(tree, options); + const normalizedOption = await normalizeOptions(tree, options, { + suffix: 'module', + }); return { ...normalizedOption, language: options.language, diff --git a/packages/nest/src/generators/module/schema.json b/packages/nest/src/generators/module/schema.json index f730ac1d1c15a..ca8189b156f08 100644 --- a/packages/nest/src/generators/module/schema.json +++ b/packages/nest/src/generators/module/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the module `FooModule` at `myapp/src/app/foo.module.ts`", + "command": "nx g @nx/nest:module myapp/src/app/foo.module.ts" + }, + { + "description": "Generate the module without providing the file extension. It results in the module `FooModule` at `myapp/src/app/foo.module.ts`", "command": "nx g @nx/nest:module myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the module without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the module. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/pipe/pipe.ts b/packages/nest/src/generators/pipe/pipe.ts index 8d6419595e761..09f921b638ade 100644 --- a/packages/nest/src/generators/pipe/pipe.ts +++ b/packages/nest/src/generators/pipe/pipe.ts @@ -28,7 +28,9 @@ async function normalizePipeOptions( tree: Tree, options: PipeGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'pipe', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/pipe/schema.json b/packages/nest/src/generators/pipe/schema.json index f7da5a8c95dea..0e1058eca8ec0 100644 --- a/packages/nest/src/generators/pipe/schema.json +++ b/packages/nest/src/generators/pipe/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the pipe `FooPipe` at `myapp/src/app/foo.pipe.ts`", + "command": "nx g @nx/nest:pipe myapp/src/app/foo.pipe.ts" + }, + { + "description": "Generate the pipe without providing the file extension. It results in the pipe `FooPipe` at `myapp/src/app/foo.pipe.ts`", "command": "nx g @nx/nest:pipe myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the pipe without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the pipe. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/provider/schema.json b/packages/nest/src/generators/provider/schema.json index ab1bcf9472ed1..d4269cac641fe 100644 --- a/packages/nest/src/generators/provider/schema.json +++ b/packages/nest/src/generators/provider/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the provider `Foo` at `myapp/src/app/foo.ts`", + "command": "nx g @nx/nest:provider myapp/src/app/foo.ts" + }, + { + "description": "Generate the provider without providing the file extension. It results in the provider `Foo` at `myapp/src/app/foo.ts`", "command": "nx g @nx/nest:provider myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the provider without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the provider. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/resolver/resolver.ts b/packages/nest/src/generators/resolver/resolver.ts index 759c2d72d9d68..880ef84c25c0d 100644 --- a/packages/nest/src/generators/resolver/resolver.ts +++ b/packages/nest/src/generators/resolver/resolver.ts @@ -28,7 +28,9 @@ async function normalizeResolverOptions( tree: Tree, options: ResolverGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'resolver', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/resolver/schema.json b/packages/nest/src/generators/resolver/schema.json index 3226b4a078fb3..86f93b37ce499 100644 --- a/packages/nest/src/generators/resolver/schema.json +++ b/packages/nest/src/generators/resolver/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the resolver `FooResolver` at `myapp/src/app/foo.resolver.ts`", + "command": "nx g @nx/nest:resolver myapp/src/app/foo.resolver.ts" + }, + { + "description": "Generate the resolver without providing the file extension. It results in the resolver `FooResolver` at `myapp/src/app/foo.resolver.ts`", "command": "nx g @nx/nest:resolver myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the resolver without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the resolver. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/resource/resource.ts b/packages/nest/src/generators/resource/resource.ts index f3896ef8fa4d3..1f1301f9efdad 100644 --- a/packages/nest/src/generators/resource/resource.ts +++ b/packages/nest/src/generators/resource/resource.ts @@ -1,6 +1,5 @@ import type { Tree } from '@nx/devkit'; import type { - NestGeneratorWithLanguageOption, NestGeneratorWithResourceOption, NestGeneratorWithTestOption, NormalizedOptions, @@ -11,8 +10,7 @@ import { unitTestRunnerToSpec, } from '../utils'; -export type ResourceGeneratorOptions = NestGeneratorWithLanguageOption & - NestGeneratorWithTestOption & +export type ResourceGeneratorOptions = NestGeneratorWithTestOption & NestGeneratorWithResourceOption; export async function resourceGenerator( @@ -30,10 +28,11 @@ async function normalizeResourceOptions( tree: Tree, options: ResourceGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + skipLanguageOption: true, + }); return { ...normalizedOptions, - language: options.language, spec: unitTestRunnerToSpec(options.unitTestRunner), }; } diff --git a/packages/nest/src/generators/resource/schema.json b/packages/nest/src/generators/resource/schema.json index 304567d33d38a..74a2725fb0d0e 100644 --- a/packages/nest/src/generators/resource/schema.json +++ b/packages/nest/src/generators/resource/schema.json @@ -14,7 +14,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the resource without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the resource. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 @@ -33,11 +33,6 @@ "enum": ["jest", "none"], "default": "jest" }, - "language": { - "description": "Nest class language.", - "type": "string", - "enum": ["js", "ts"] - }, "type": { "type": "string", "description": "The transport layer.", diff --git a/packages/nest/src/generators/service/schema.json b/packages/nest/src/generators/service/schema.json index 4d96311886dfd..4e0b8ec246175 100644 --- a/packages/nest/src/generators/service/schema.json +++ b/packages/nest/src/generators/service/schema.json @@ -8,12 +8,16 @@ "examples": [ { "description": "Generate the service `FooService` at `myapp/src/app/foo.service.ts`", + "command": "nx g @nx/nest:service myapp/src/app/foo.service.ts" + }, + { + "description": "Generate the service without providing the file extension. It results in the service `FooService` at `myapp/src/app/foo.service.ts`", "command": "nx g @nx/nest:service myapp/src/app/foo" } ], "properties": { "path": { - "description": "The file path to the service without the file extension and suffix. Relative to the current working directory.", + "description": "The file path to the service. Relative to the current working directory.", "type": "string", "$default": { "$source": "argv", diff --git a/packages/nest/src/generators/service/service.ts b/packages/nest/src/generators/service/service.ts index ad4229fbfaa27..504b3434f960b 100644 --- a/packages/nest/src/generators/service/service.ts +++ b/packages/nest/src/generators/service/service.ts @@ -28,7 +28,9 @@ async function normalizeServiceOptions( tree: Tree, options: ServiceGeneratorOptions ): Promise { - const normalizedOptions = await normalizeOptions(tree, options); + const normalizedOptions = await normalizeOptions(tree, options, { + suffix: 'service', + }); return { ...normalizedOptions, language: options.language, diff --git a/packages/nest/src/generators/utils/normalize-options.ts b/packages/nest/src/generators/utils/normalize-options.ts index cd0d2baa063dd..c9012b5972869 100644 --- a/packages/nest/src/generators/utils/normalize-options.ts +++ b/packages/nest/src/generators/utils/normalize-options.ts @@ -1,27 +1,53 @@ import type { Tree } from '@nx/devkit'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import type { - NestGeneratorOptions, + Language, + NestGeneratorWithLanguageOption, NormalizedOptions, UnitTestRunner, } from './types'; -import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; export async function normalizeOptions( tree: Tree, - options: NestGeneratorOptions + options: NestGeneratorWithLanguageOption, + normalizationOptions: { + allowedFileExtensions?: Array<'js' | 'ts'>; + skipLanguageOption?: boolean; + suffix?: string; + } = {} ): Promise { - const { directory, artifactName } = + const { + allowedFileExtensions = ['js', 'ts'], + skipLanguageOption = false, + suffix, + } = normalizationOptions; + + const { directory, artifactName, fileExtension } = await determineArtifactNameAndDirectoryOptions(tree, { path: options.path, + allowedFileExtensions, + fileExtension: options.language === 'js' ? 'js' : 'ts', + js: options.language ? options.language === 'js' : undefined, + jsOptionName: 'language', }); options.path = undefined; // Now that we have `directory` we don't need `path` + if (!skipLanguageOption) { + // we assign the language based on the normalized file extension + options.language = fileExtension as Language; + } + + let name = artifactName; + if (suffix && artifactName.endsWith(`.${suffix}`)) { + // strip the suffix if it exists, the nestjs schematic will always add it + name = artifactName.replace(`.${suffix}`, ''); + } + return { ...options, flat: true, - name: artifactName, - skipFormat: options.skipFormat, + name, sourceRoot: directory, }; } diff --git a/packages/next/docs/component-examples.md b/packages/next/docs/component-examples.md index 44d508d5ba3b0..d604608650f19 100644 --- a/packages/next/docs/component-examples.md +++ b/packages/next/docs/component-examples.md @@ -6,7 +6,7 @@ Generate a component named `MyComponent` at `apps/my-app/src/app/my-component/my-component.tsx`: ```shell -nx g component apps/my-app/src/app/my-component/my-component +nx g component apps/my-app/src/app/my-component/my-component.tsx ``` {% /tab %} @@ -15,7 +15,16 @@ nx g component apps/my-app/src/app/my-component/my-component Generate a component named `Custom` at `apps/my-app/src/app/my-component/my-component.tsx`: ```shell -nx g component apps/my-app/src/app/my-component/my-component --name=custom +nx g component apps/my-app/src/app/my-component/my-component.tsx --name=custom +``` + +{% /tab %} +{% tab label="Create a Component Omitting the File Extension" %} + +Generate a component named `MyComponent` at `apps/my-app/src/app/my-component/my-component.tsx` without specifying the file extension: + +```shell +nx g component apps/my-app/src/app/my-component/my-component ``` {% /tab %} diff --git a/packages/next/src/generators/component/component.spec.ts b/packages/next/src/generators/component/component.spec.ts index 7ee0934f6b802..d89527922f539 100644 --- a/packages/next/src/generators/component/component.spec.ts +++ b/packages/next/src/generators/component/component.spec.ts @@ -40,6 +40,19 @@ describe('component', () => { ).toBeTruthy(); }); + it('should handle path with file extension', async () => { + await componentGenerator(tree, { + path: `${appName}/components/hello/hello.tsx`, + style: 'css', + }); + + expect(tree.exists('my-app/components/hello/hello.tsx')).toBeTruthy(); + expect(tree.exists('my-app/components/hello/hello.spec.tsx')).toBeTruthy(); + expect( + tree.exists('my-app/components/hello/hello.module.css') + ).toBeTruthy(); + }); + it('should generate component in default directory for library', async () => { await componentGenerator(tree, { name: 'hello', diff --git a/packages/next/src/generators/component/component.ts b/packages/next/src/generators/component/component.ts index fd50c5dc0a77a..f5cc5a03ba8c4 100644 --- a/packages/next/src/generators/component/component.ts +++ b/packages/next/src/generators/component/component.ts @@ -1,16 +1,14 @@ import { formatFiles, - getProjects, joinPathFragments, readProjectConfiguration, runTasksInSerial, Tree, } from '@nx/devkit'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import type { SupportedStyles } from '@nx/react'; import { componentGenerator as reactComponentGenerator } from '@nx/react'; - import { addStyleDependencies } from '../../utils/styles'; -import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; interface Schema { path: string; @@ -24,19 +22,15 @@ interface Schema { * extra dependencies for css, sass, less style options. */ export async function componentGenerator(host: Tree, options: Schema) { - const { - artifactName: name, - directory, - project: projectName, - } = await determineArtifactNameAndDirectoryOptions(host, { - name: options.name, - path: options.path, - fileExtension: 'tsx', - }); + // we only need to provide the path to get the project, we let the react + // generator handle the rest + const { project: projectName } = + await determineArtifactNameAndDirectoryOptions(host, { + path: options.path, + }); const componentInstall = await reactComponentGenerator(host, { ...options, - name, classComponent: false, routing: false, skipFormat: true, diff --git a/packages/next/src/generators/component/schema.json b/packages/next/src/generators/component/schema.json index feed4a894384a..c35a59782b446 100644 --- a/packages/next/src/generators/component/schema.json +++ b/packages/next/src/generators/component/schema.json @@ -8,7 +8,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 @@ -75,7 +75,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipFormat": { "description": "Skip formatting files.", diff --git a/packages/next/src/generators/page/page.ts b/packages/next/src/generators/page/page.ts index 95f16f6fa9c07..918c4ddd15fe4 100644 --- a/packages/next/src/generators/page/page.ts +++ b/packages/next/src/generators/page/page.ts @@ -80,20 +80,17 @@ async function normalizeOptions(host: Tree, options: Schema) { } } - // the helper below expects a path to a file, but the `path` option here - // represents a directory, so we artificially add the symbol name to it - options.path = joinPathFragments(options.path, pageSymbolName); - - const { project: projectName, fileName } = + const { project: projectName, filePath } = await determineArtifactNameAndDirectoryOptions(host, { name: pageSymbolName, - fileName: isAppRouter ? 'page' : 'index', - path: options.path, - fileExtension: 'tsx', + path: joinPathFragments( + options.path, + isAppRouter ? 'page.tsx' : 'index.tsx' + ), }); return { ...options, - fileName, + path: filePath, projectName, }; } diff --git a/packages/plugin/docs/generators/executor-examples.md b/packages/plugin/docs/generators/executor-examples.md index a904ba6d86caf..026385429c72d 100644 --- a/packages/plugin/docs/generators/executor-examples.md +++ b/packages/plugin/docs/generators/executor-examples.md @@ -5,6 +5,15 @@ Create a new executor called `build` at `tools/my-plugin/src/executors/build.ts`: +```bash +nx g @nx/plugin:executor tools/my-plugin/src/executors/build.ts +``` + +{% /tab %} +{% tab label="Without providing the file extension" %} + +Create a new executor called `build` at `tools/my-plugin/src/executors/build.ts`: + ```bash nx g @nx/plugin:executor tools/my-plugin/src/executors/build ``` @@ -15,7 +24,7 @@ nx g @nx/plugin:executor tools/my-plugin/src/executors/build Create a new executor called `custom` at `tools/my-plugin/src/executors/build.ts`: ```bash -nx g @nx/plugin:executor tools/my-plugin/src/executors/build --name=custom +nx g @nx/plugin:executor tools/my-plugin/src/executors/build.ts --name=custom ``` {% /tab %} diff --git a/packages/plugin/src/generators/executor/executor.spec.ts b/packages/plugin/src/generators/executor/executor.spec.ts index b08c1327c354a..24fd9de440bf8 100644 --- a/packages/plugin/src/generators/executor/executor.spec.ts +++ b/packages/plugin/src/generators/executor/executor.spec.ts @@ -47,6 +47,28 @@ describe('NxPlugin Executor Generator', () => { ).toBeTruthy(); }); + it('should handle path with file extension', async () => { + await executorGenerator(tree, { + name: 'my-executor', + path: 'my-plugin/src/executors/my-executor/executor.ts', + unitTestRunner: 'jest', + includeHasher: false, + }); + + expect( + tree.exists('my-plugin/src/executors/my-executor/schema.d.ts') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/executors/my-executor/schema.json') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/executors/my-executor/executor.ts') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/executors/my-executor/executor.spec.ts') + ).toBeTruthy(); + }); + it('should generate files relative to the cwd', async () => { setCwd('my-plugin/src/executors/my-executor'); await executorGenerator(tree, { diff --git a/packages/plugin/src/generators/executor/executor.ts b/packages/plugin/src/generators/executor/executor.ts index 92a61691a59bc..46154a161baec 100644 --- a/packages/plugin/src/generators/executor/executor.ts +++ b/packages/plugin/src/generators/executor/executor.ts @@ -164,6 +164,8 @@ async function normalizeOptions( } = await determineArtifactNameAndDirectoryOptions(tree, { path: options.path, name: options.name, + allowedFileExtensions: ['ts'], + fileExtension: 'ts', }); const { className, propertyName } = names(name); diff --git a/packages/plugin/src/generators/executor/schema.json b/packages/plugin/src/generators/executor/schema.json index 2ee691e2bc859..f9c513abb5b63 100644 --- a/packages/plugin/src/generators/executor/schema.json +++ b/packages/plugin/src/generators/executor/schema.json @@ -9,7 +9,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the executor without the file extension. Relative to the current working directory.", + "description": "The file path to the executor. Relative to the current working directory.", "x-prompt": "What is the executor file path?", "$default": { "$source": "argv", diff --git a/packages/plugin/src/generators/generator/generator.spec.ts b/packages/plugin/src/generators/generator/generator.spec.ts index 619249c8fa338..6a2a460c9755e 100644 --- a/packages/plugin/src/generators/generator/generator.spec.ts +++ b/packages/plugin/src/generators/generator/generator.spec.ts @@ -50,6 +50,27 @@ describe('NxPlugin Generator Generator', () => { ).toBeTruthy(); }); + it('should handle path with file extension', async () => { + await generatorGenerator(tree, { + name: 'my-generator', + path: 'my-plugin/src/generators/my-generator/generator.ts', + unitTestRunner: 'jest', + }); + + expect( + tree.exists('my-plugin/src/generators/my-generator/schema.d.ts') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/generators/my-generator/schema.json') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/generators/my-generator/generator.ts') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/generators/my-generator/generator.spec.ts') + ).toBeTruthy(); + }); + it('should generate files relative to cwd', async () => { setCwd('my-plugin/src/nx-integrations/generators/my-generator'); await generatorGenerator(tree, { diff --git a/packages/plugin/src/generators/generator/generator.ts b/packages/plugin/src/generators/generator/generator.ts index 45a9469a98c36..a96220ace4e02 100644 --- a/packages/plugin/src/generators/generator/generator.ts +++ b/packages/plugin/src/generators/generator/generator.ts @@ -43,6 +43,8 @@ async function normalizeOptions( } = await determineArtifactNameAndDirectoryOptions(tree, { path: options.path, name: options.name, + allowedFileExtensions: ['ts'], + fileExtension: 'ts', }); const { className, propertyName } = names(name); diff --git a/packages/plugin/src/generators/generator/schema.json b/packages/plugin/src/generators/generator/schema.json index 206b0e0a1e1f0..9c94dbb5f0f0d 100644 --- a/packages/plugin/src/generators/generator/schema.json +++ b/packages/plugin/src/generators/generator/schema.json @@ -8,6 +8,10 @@ "examples": [ { "description": "Generate a generator exported with the name matching the file name. It results in the generator `foo` at `mylib/src/generators/foo.ts`", + "command": "nx g @nx/plugin:generator mylib/src/generators/foo.ts" + }, + { + "description": "Generate a generator without providing the file extension. It results in the generator `foo` at `mylib/src/generators/foo.ts`", "command": "nx g @nx/plugin:generator mylib/src/generators/foo" }, { @@ -18,7 +22,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the generator without the file extension. Relative to the current working directory.", + "description": "The file path to the generator. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 diff --git a/packages/plugin/src/generators/migration/migration.spec.ts b/packages/plugin/src/generators/migration/migration.spec.ts index 53ad5d02294ae..23b5403492f43 100644 --- a/packages/plugin/src/generators/migration/migration.spec.ts +++ b/packages/plugin/src/generators/migration/migration.spec.ts @@ -68,6 +68,34 @@ describe('NxPlugin migration generator', () => { ); }); + it('should handle path with file extension', async () => { + await migrationGenerator(tree, { + name: 'my-migration', + path: 'packages/my-plugin/migrations/1.0.0/my-migration.ts', + packageVersion: '1.0.0', + }); + + const migrationsJson = readJson(tree, 'packages/my-plugin/migrations.json'); + const packageJson = readJson(tree, 'packages/my-plugin/package.json'); + + expect( + tree.exists('packages/my-plugin/migrations/1.0.0/my-migration.ts') + ).toBeTruthy(); + + expect(migrationsJson.generators['my-migration'].version).toEqual('1.0.0'); + expect(migrationsJson.generators['my-migration'].description).toEqual( + 'Migration for v1.0.0' + ); + expect(migrationsJson.generators['my-migration'].implementation).toEqual( + './migrations/1.0.0/my-migration' + ); + expect(migrationsJson.packageJsonUpdates).toBeFalsy(); + + expect(packageJson['nx-migrations'].migrations).toEqual( + './migrations.json' + ); + }); + it('should generate files with default name', async () => { await migrationGenerator(tree, { description: 'my-migration description', diff --git a/packages/plugin/src/generators/migration/migration.ts b/packages/plugin/src/generators/migration/migration.ts index 1429fa6e49d9b..d797146c2fb41 100644 --- a/packages/plugin/src/generators/migration/migration.ts +++ b/packages/plugin/src/generators/migration/migration.ts @@ -39,6 +39,8 @@ async function normalizeOptions( } = await determineArtifactNameAndDirectoryOptions(tree, { path: options.path, name: options.name, + allowedFileExtensions: ['ts'], + fileExtension: 'ts', }); const { root: projectRoot, sourceRoot: projectSourceRoot } = diff --git a/packages/plugin/src/generators/migration/schema.json b/packages/plugin/src/generators/migration/schema.json index 7c158677e7b3a..17546e8e64567 100644 --- a/packages/plugin/src/generators/migration/schema.json +++ b/packages/plugin/src/generators/migration/schema.json @@ -8,6 +8,10 @@ "examples": [ { "description": "Generate a migration exported with the name matching the file name, which will be triggered when migrating to version 1.0.0 or above from a previous version. It results in the migration `foo` at `mylib/src/migrations/foo.ts`", + "command": "nx g @nx/plugin:migration mylib/src/migrations/foo.ts -v=1.0.0" + }, + { + "description": "Generate a migration without providing the file extension, which will be triggered when migrating to version 1.0.0 or above from a previous version. It results in the migration `foo` at `mylib/src/migrations/foo.ts`", "command": "nx g @nx/plugin:migration mylib/src/migrations/foo -v=1.0.0" }, { diff --git a/packages/react-native/src/generators/component/component.spec.ts b/packages/react-native/src/generators/component/component.spec.ts index 1bbf2190f44c9..b231d37f43884 100644 --- a/packages/react-native/src/generators/component/component.spec.ts +++ b/packages/react-native/src/generators/component/component.spec.ts @@ -33,6 +33,15 @@ describe('component', () => { expect(appTree.exists('my-lib/src/lib/hello/hello.spec.tsx')).toBeTruthy(); }); + it('should handle path with file extension', async () => { + await reactNativeComponentGenerator(appTree, { + path: `${projectName}/src/lib/hello/hello.tsx`, + }); + + expect(appTree.exists('my-lib/src/lib/hello/hello.tsx')).toBeTruthy(); + expect(appTree.exists('my-lib/src/lib/hello/hello.spec.tsx')).toBeTruthy(); + }); + it('should generate files for an app', async () => { await reactNativeComponentGenerator(appTree, { name: 'hello', diff --git a/packages/react-native/src/generators/component/component.ts b/packages/react-native/src/generators/component/component.ts index 83f1177c2001c..8cae0a4e7942b 100644 --- a/packages/react-native/src/generators/component/component.ts +++ b/packages/react-native/src/generators/component/component.ts @@ -5,7 +5,6 @@ import { generateFiles, getProjects, joinPathFragments, - toJS, Tree, } from '@nx/devkit'; import { NormalizedSchema, normalizeOptions } from './lib/normalize-options'; @@ -26,25 +25,23 @@ export async function reactNativeComponentGenerator( } function createComponentFiles(host: Tree, options: NormalizedSchema) { - generateFiles(host, join(__dirname, './files'), options.directory, { - ...options, - tmpl: '', - }); - - for (const c of host.listChanges()) { - let deleteFile = false; - - if (options.skipTests && /.*spec.tsx/.test(c.path)) { - deleteFile = true; + generateFiles( + host, + join(__dirname, 'files', options.fileExtensionType), + options.directory, + { + ...options, + ext: options.fileExtension, } + ); - if (deleteFile) { - host.delete(c.path); - } - } - - if (options.js) { - toJS(host); + if (options.skipTests) { + host.delete( + joinPathFragments( + options.directory, + `${options.fileName}.spec.${options.fileExtension}` + ) + ); } } @@ -61,27 +58,30 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) { if (options.export && !isApp) { const indexFilePath = joinPathFragments( options.projectSourceRoot, - options.js ? 'index.js' : 'index.ts' + options.fileExtensionType === 'js' ? 'index.js' : 'index.ts' ); - const indexSource = host.read(indexFilePath, 'utf-8'); - if (indexSource !== null) { - const indexSourceFile = tsModule.createSourceFile( - indexFilePath, - indexSource, - tsModule.ScriptTarget.Latest, - true - ); - const relativePathFromIndex = getRelativeImportToFile( - indexFilePath, - options.filePath - ); - const changes = applyChangesToString( - indexSource, - addImport(indexSourceFile, `export * from '${relativePathFromIndex}';`) - ); - host.write(indexFilePath, changes); + if (!host.exists(indexFilePath)) { + return; } + + const indexSource = host.read(indexFilePath, 'utf-8'); + const indexSourceFile = tsModule.createSourceFile( + indexFilePath, + indexSource, + tsModule.ScriptTarget.Latest, + true + ); + + const relativePathFromIndex = getRelativeImportToFile( + indexFilePath, + options.filePath + ); + const changes = applyChangesToString( + indexSource, + addImport(indexSourceFile, `export * from '${relativePathFromIndex}';`) + ); + host.write(indexFilePath, changes); } } diff --git a/packages/react-native/src/generators/component/files/js/__fileName__.__ext__ b/packages/react-native/src/generators/component/files/js/__fileName__.__ext__ new file mode 100644 index 0000000000000..37f4f213c2a1f --- /dev/null +++ b/packages/react-native/src/generators/component/files/js/__fileName__.__ext__ @@ -0,0 +1,28 @@ +<%_ if (classComponent) { _%> +import { Component } from 'react'; +<%_ } else { _%> +import React from 'react'; +<%_ } _%> +import { View, Text } from 'react-native'; + +<%_ if (classComponent) { _%> +export class <%= className %> extends Component { + render() { + return ( + + Welcome to <%= name %>! + + ); + } +} +<%_ } else { _%> +export function <%= className %>(props) { + return ( + + Welcome to <%= name %>! + + ); +} +<%_ } _%> + +export default <%= className %>; diff --git a/packages/expo/src/generators/component/files/__fileName__.spec.tsx.template b/packages/react-native/src/generators/component/files/js/__fileName__.spec.__ext__ similarity index 100% rename from packages/expo/src/generators/component/files/__fileName__.spec.tsx.template rename to packages/react-native/src/generators/component/files/js/__fileName__.spec.__ext__ diff --git a/packages/react-native/src/generators/component/files/__fileName__.tsx__tmpl__ b/packages/react-native/src/generators/component/files/ts/__fileName__.__ext__ similarity index 82% rename from packages/react-native/src/generators/component/files/__fileName__.tsx__tmpl__ rename to packages/react-native/src/generators/component/files/ts/__fileName__.__ext__ index f67db2c698ff2..b66220b95ddf3 100644 --- a/packages/react-native/src/generators/component/files/__fileName__.tsx__tmpl__ +++ b/packages/react-native/src/generators/component/files/ts/__fileName__.__ext__ @@ -1,15 +1,15 @@ -<% if (classComponent) { %> +<%_ if (classComponent) { _%> import { Component } from 'react'; -<% } else { %> +<%_ } else { _%> import React from 'react'; -<% } %> +<%_ } _%> import { View, Text } from 'react-native'; /* eslint-disable-next-line */ export interface <%= className %>Props { } -<% if (classComponent) { %> +<%_ if (classComponent) { _%> export class <%= className %> extends Component<<%= className %>Props> { render() { return ( @@ -19,7 +19,7 @@ export class <%= className %> extends Component<<%= className %>Props> { ); } } -<% } else { %> +<%_ } else { _%> export function <%= className %>(props: <%= className %>Props) { return ( @@ -27,6 +27,6 @@ export function <%= className %>(props: <%= className %>Props) { ); } -<% } %> +<%_ } _%> export default <%= className %>; diff --git a/packages/react-native/src/generators/component/files/__fileName__.spec.tsx__tmpl__ b/packages/react-native/src/generators/component/files/ts/__fileName__.spec.__ext__ similarity index 100% rename from packages/react-native/src/generators/component/files/__fileName__.spec.tsx__tmpl__ rename to packages/react-native/src/generators/component/files/ts/__fileName__.spec.__ext__ diff --git a/packages/react-native/src/generators/component/lib/normalize-options.ts b/packages/react-native/src/generators/component/lib/normalize-options.ts index e9426cebb4047..9aa857e88033e 100644 --- a/packages/react-native/src/generators/component/lib/normalize-options.ts +++ b/packages/react-native/src/generators/component/lib/normalize-options.ts @@ -1,13 +1,18 @@ import { getProjects, logger, names, Tree } from '@nx/devkit'; +import { + determineArtifactNameAndDirectoryOptions, + type FileExtensionType, +} from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { Schema } from '../schema'; -import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; -export interface NormalizedSchema extends Schema { +export interface NormalizedSchema extends Omit { directory: string; projectSourceRoot: string; fileName: string; className: string; filePath: string; + fileExtension: string; + fileExtensionType: FileExtensionType; projectName: string; } @@ -20,15 +25,17 @@ export async function normalizeOptions( directory, fileName, filePath, + fileExtension, + fileExtensionType, project: projectName, } = await determineArtifactNameAndDirectoryOptions(host, { path: options.path, name: options.name, - fileExtension: 'tsx', + allowedFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + fileExtension: options.js ? 'js' : 'tsx', + js: options.js, }); - assertValidOptions({ name, directory }); - const project = getProjects(host).get(projectName); const { className } = names(name); @@ -50,23 +57,9 @@ export async function normalizeOptions( className, fileName, filePath, + fileExtension, + fileExtensionType, projectSourceRoot, projectName, }; } - -function assertValidOptions(options: { name: string; directory: string }) { - const slashes = ['/', '\\']; - slashes.forEach((s) => { - if (options.name.indexOf(s) !== -1) { - const [name, ...rest] = options.name.split(s).reverse(); - let suggestion = rest.map((x) => x.toLowerCase()).join(s); - if (options.directory) { - suggestion = `${options.directory}${s}${suggestion}`; - } - throw new Error( - `Found "${s}" in the component name. Did you mean to use the --path option (e.g. \`nx g c ${suggestion}/${name} \`)?` - ); - } - }); -} diff --git a/packages/react-native/src/generators/component/schema.d.ts b/packages/react-native/src/generators/component/schema.d.ts index 6e6edb5ec3346..b0b95f7b1deb2 100644 --- a/packages/react-native/src/generators/component/schema.d.ts +++ b/packages/react-native/src/generators/component/schema.d.ts @@ -7,5 +7,9 @@ export interface Schema { skipTests?: boolean; export?: boolean; classComponent?: boolean; + + /** + * @deprecated Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21. + */ js?: boolean; } diff --git a/packages/react-native/src/generators/component/schema.json b/packages/react-native/src/generators/component/schema.json index adde83800b39f..486ff3e00b9eb 100644 --- a/packages/react-native/src/generators/component/schema.json +++ b/packages/react-native/src/generators/component/schema.json @@ -8,11 +8,15 @@ "examples": [ { "description": "Generate a component with the exported symbol matching the file name. It results in the component `Foo` at `mylib/src/lib/foo.tsx`", - "command": "nx g @nx/react-native:component mylib/src/lib/foo" + "command": "nx g @nx/react-native:component mylib/src/lib/foo.tsx" }, { "description": "Generate a component with the exported symbol different from the file name. It results in the component `Custom` at `mylib/src/lib/foo.tsx`", - "command": "nx g @nx/react-native:component mylib/src/lib/foo --name=custom" + "command": "nx g @nx/react-native:component mylib/src/lib/foo.tsx --name=custom" + }, + { + "description": "Generate a component without providing the file extension. It results in the component `Foo` at `mylib/src/lib/foo.tsx`", + "command": "nx g @nx/react-native:component mylib/src/lib/foo" }, { "description": "Generate a class component at `mylib/src/lib/foo.tsx`", @@ -22,7 +26,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 @@ -36,7 +40,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipTests": { "type": "boolean", diff --git a/packages/react/docs/component-examples.md b/packages/react/docs/component-examples.md index efbc7daea600c..e207da42d1e6a 100644 --- a/packages/react/docs/component-examples.md +++ b/packages/react/docs/component-examples.md @@ -6,7 +6,7 @@ Create a component named `MyComponent` at `libs/ui/src/my-component.tsx`: ```shell -nx g @nx/react:component libs/ui/src/my-component +nx g @nx/react:component libs/ui/src/my-component.tsx ``` {% /tab %} @@ -16,7 +16,17 @@ nx g @nx/react:component libs/ui/src/my-component Create a component named `Custom` at `libs/ui/src/my-component.tsx`: ```shell -nx g @nx/react:component libs/ui/src/my-component --name=custom +nx g @nx/react:component libs/ui/src/my-component.tsx --name=custom +``` + +{% /tab %} + +{% tab label="Omitting the File Extension" %} + +Create a component named `MyComponent` at `libs/ui/src/my-component.tsx` without specifying the file extension: + +```shell +nx g @nx/react:component libs/ui/src/my-component ``` {% /tab %} diff --git a/packages/react/src/generators/component/component.spec.ts b/packages/react/src/generators/component/component.spec.ts index 76555e45f042d..acb66237ed83c 100644 --- a/packages/react/src/generators/component/component.spec.ts +++ b/packages/react/src/generators/component/component.spec.ts @@ -57,6 +57,26 @@ describe('component', () => { ); }); + it('should handle path with file extension', async () => { + await componentGenerator(appTree, { + name: 'hello', + style: 'css', + path: `${projectName}/src/lib/hello/hello.tsx`, + }); + + expect(appTree.exists('my-lib/src/lib/hello/hello.tsx')).toBeTruthy(); + expect(appTree.exists('my-lib/src/lib/hello/hello.spec.tsx')).toBeTruthy(); + expect( + appTree.exists('my-lib/src/lib/hello/hello.module.css') + ).toBeTruthy(); + expect(appTree.read('my-lib/src/lib/hello/hello.tsx').toString()).toMatch( + /import styles from '.\/hello.module.css'/ + ); + expect(appTree.read('my-lib/src/lib/hello/hello.tsx').toString()).toMatch( + /
/ + ); + }); + it('should generate files with global CSS', async () => { await componentGenerator(appTree, { name: 'hello', diff --git a/packages/react/src/generators/component/component.ts b/packages/react/src/generators/component/component.ts index d532e074062fb..90453bf07ebf8 100644 --- a/packages/react/src/generators/component/component.ts +++ b/packages/react/src/generators/component/component.ts @@ -7,7 +7,6 @@ import { getProjects, joinPathFragments, runTasksInSerial, - toJS, Tree, } from '@nx/devkit'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; @@ -54,44 +53,29 @@ function createComponentFiles(host: Tree, options: NormalizedSchema) { ...options, componentTests, inSourceVitestTests: getInSourceVitestTestsTemplate(componentTests), - tmpl: '', + isTs: options.fileExtensionType === 'ts', + ext: options.fileExtension, }); - for (const c of host.listChanges()) { - let deleteFile = false; - - if ( - (options.skipTests || options.inSourceTests) && - /.*spec.tsx/.test(c.path) - ) { - deleteFile = true; - } - - if ( - (options.styledModule || !options.hasStyles) && - c.path.endsWith(`.${options.style}`) - ) { - deleteFile = true; - } - - if (options.globalCss && c.path.endsWith(`.module.${options.style}`)) { - deleteFile = true; - } - - if ( - !options.globalCss && - c.path.endsWith(`${options.fileName}.${options.style}`) - ) { - deleteFile = true; - } + if (options.skipTests || options.inSourceTests) { + host.delete( + joinPathFragments( + options.directory, + `${options.fileName}.spec.${options.fileExtension}` + ) + ); + } - if (deleteFile) { - host.delete(c.path); - } + if (options.styledModule || !options.hasStyles || !options.globalCss) { + host.delete( + join(options.directory, `${options.fileName}.${options.style}`) + ); } - if (options.js) { - toJS(host); + if (options.styledModule || !options.hasStyles || options.globalCss) { + host.delete( + join(options.directory, `${options.fileName}.module.${options.style}`) + ); } } @@ -106,35 +90,33 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) { workspace.get(options.projectName).projectType === 'application'; if (options.export && !isApp) { - const indexFilePath = options.projectSourceRoot - ? joinPathFragments( - options.projectSourceRoot, - options.js ? 'index.js' : 'index.ts' - ) - : joinPathFragments( - options.projectRoot, - 'src', - options.js ? 'index.js' : 'index.ts' - ); + const indexFilePath = joinPathFragments( + ...(options.projectSourceRoot + ? [options.projectSourceRoot] + : [options.projectRoot, 'src']), + options.fileExtensionType === 'js' ? 'index.js' : 'index.ts' + ); - const indexSource = host.read(indexFilePath, 'utf-8'); - if (indexSource !== null) { - const indexSourceFile = tsModule.createSourceFile( - indexFilePath, - indexSource, - tsModule.ScriptTarget.Latest, - true - ); - const relativePathFromIndex = getRelativeImportToFile( - indexFilePath, - options.filePath - ); - const changes = applyChangesToString( - indexSource, - addImport(indexSourceFile, `export * from '${relativePathFromIndex}';`) - ); - host.write(indexFilePath, changes); + if (!host.exists(indexFilePath)) { + return; } + + const indexSource = host.read(indexFilePath, 'utf-8'); + const indexSourceFile = tsModule.createSourceFile( + indexFilePath, + indexSource, + tsModule.ScriptTarget.Latest, + true + ); + const relativePathFromIndex = getRelativeImportToFile( + indexFilePath, + options.filePath + ); + const changes = applyChangesToString( + indexSource, + addImport(indexSourceFile, `export * from '${relativePathFromIndex}';`) + ); + host.write(indexFilePath, changes); } } diff --git a/packages/react/src/generators/component/files/__fileName__.tsx__tmpl__ b/packages/react/src/generators/component/files/__fileName__.__ext__ similarity index 95% rename from packages/react/src/generators/component/files/__fileName__.tsx__tmpl__ rename to packages/react/src/generators/component/files/__fileName__.__ext__ index 60bfe0356c994..36e119543f5d6 100644 --- a/packages/react/src/generators/component/files/__fileName__.tsx__tmpl__ +++ b/packages/react/src/generators/component/files/__fileName__.__ext__ @@ -29,8 +29,8 @@ const Styled<%= className %> = styled.div` <%_ } _%> <%_ if(!isNextPage) { _%> <%_ if (classComponent) { _%> -export class <%= className %> extends Component<{}> { - override render() { +export class <%= className %> extends Component<% if (isTs) { %><{}><% } %> { + <% if (isTs) { %>override<% } %> render() { return ( <<%= wrapper %><%- extras %>> <%= styledModule === 'styled-jsx' ? `` : `` %> diff --git a/packages/react/src/generators/component/files/__fileName__.spec.tsx__tmpl__ b/packages/react/src/generators/component/files/__fileName__.spec.__ext__ similarity index 100% rename from packages/react/src/generators/component/files/__fileName__.spec.tsx__tmpl__ rename to packages/react/src/generators/component/files/__fileName__.spec.__ext__ diff --git a/packages/react/src/generators/component/lib/normalize-options.ts b/packages/react/src/generators/component/lib/normalize-options.ts index 3ea86c7d1c493..49b36dfc9f1f2 100644 --- a/packages/react/src/generators/component/lib/normalize-options.ts +++ b/packages/react/src/generators/component/lib/normalize-options.ts @@ -16,12 +16,15 @@ export async function normalizeOptions( directory, fileName, filePath, + fileExtension, + fileExtensionType, project: projectName, } = await determineArtifactNameAndDirectoryOptions(tree, { path: options.path, name: options.name, - fileExtension: 'tsx', - fileName: options.fileName, + allowedFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + fileExtension: options.js ? 'js' : 'tsx', + js: options.js, }); const project = readProjectConfiguration(tree, projectName); @@ -62,6 +65,8 @@ export async function normalizeOptions( fileName, filePath, projectRoot, - projectSourceRoot: projectSourceRoot, + projectSourceRoot, + fileExtension, + fileExtensionType, }; } diff --git a/packages/react/src/generators/component/schema.d.ts b/packages/react/src/generators/component/schema.d.ts index f6558748bcd16..fe111a24a64d5 100644 --- a/packages/react/src/generators/component/schema.d.ts +++ b/packages/react/src/generators/component/schema.d.ts @@ -1,3 +1,4 @@ +import type { FileExtensionType } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { SupportedStyles } from '../../../typings/style'; export interface Schema { @@ -8,22 +9,27 @@ export interface Schema { export?: boolean; classComponent?: boolean; routing?: boolean; - js?: boolean; globalCss?: boolean; - fileName?: string; inSourceTests?: boolean; skipFormat?: boolean; // Used by Next.js to determine how React should generate the page isNextPage?: boolean; + + /** + * @deprecated Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21. + */ + js?: boolean; } -export interface NormalizedSchema extends Schema { +export interface NormalizedSchema extends Omit { directory: string; projectRoot: string; projectSourceRoot: string; projectName: string; fileName: string; filePath: string; + fileExtension: string; + fileExtensionType: FileExtensionType; className: string; styledModule: null | string; hasStyles: boolean; diff --git a/packages/react/src/generators/component/schema.json b/packages/react/src/generators/component/schema.json index e5af0634c2743..ae3a6b9c0d225 100644 --- a/packages/react/src/generators/component/schema.json +++ b/packages/react/src/generators/component/schema.json @@ -8,7 +8,7 @@ "properties": { "path": { "type": "string", - "description": "The file path to the component without the file extension. Relative to the current working directory.", + "description": "The file path to the component. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 @@ -63,7 +63,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipTests": { "type": "boolean", @@ -93,10 +93,6 @@ "description": "Default is `false`. When `true`, the component is generated with `*.css`/`*.scss` instead of `*.module.css`/`*.module.scss`.", "default": false }, - "fileName": { - "type": "string", - "description": "Create a component with this file name." - }, "inSourceTests": { "type": "boolean", "default": false, diff --git a/packages/react/src/generators/hook/files/__fileName__.ts__tmpl__ b/packages/react/src/generators/hook/files/__fileName__.__ext__ similarity index 76% rename from packages/react/src/generators/hook/files/__fileName__.ts__tmpl__ rename to packages/react/src/generators/hook/files/__fileName__.__ext__ index 7ac95c9e4a5c1..4581040f3c9e0 100644 --- a/packages/react/src/generators/hook/files/__fileName__.ts__tmpl__ +++ b/packages/react/src/generators/hook/files/__fileName__.__ext__ @@ -1,12 +1,14 @@ import { useState, useCallback } from 'react' +<%_ if (isTs) { _%> // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface <%= hookTypeName %> { count: number; increment: () => void; } +<%_ } _%> -export function <%= hookName %>(): <%= hookTypeName %> { +export function <%= hookName %>()<% if (isTs) { %>: <%= hookTypeName %><% } %> { const [count, setCount] = useState(0) const increment = useCallback(() => setCount((x) => x + 1), []) return { count, increment } diff --git a/packages/react/src/generators/hook/files/__fileName__.spec.tsx__tmpl__ b/packages/react/src/generators/hook/files/__fileName__.spec.__specExt__ similarity index 100% rename from packages/react/src/generators/hook/files/__fileName__.spec.tsx__tmpl__ rename to packages/react/src/generators/hook/files/__fileName__.spec.__specExt__ diff --git a/packages/react/src/generators/hook/hook.spec.ts b/packages/react/src/generators/hook/hook.spec.ts index f3fe55b5657fd..eff082891b945 100644 --- a/packages/react/src/generators/hook/hook.spec.ts +++ b/packages/react/src/generators/hook/hook.spec.ts @@ -34,6 +34,17 @@ describe('hook', () => { ).toBeTruthy(); }); + it('should handle path with file extension', async () => { + await hookGenerator(appTree, { + path: `${projectName}/src/lib/use-form/use-form.ts`, + }); + + expect(appTree.exists('my-lib/src/lib/use-form/use-form.ts')).toBeTruthy(); + expect( + appTree.exists('my-lib/src/lib/use-form/use-form.spec.tsx') + ).toBeTruthy(); + }); + it('should generate files for an app', async () => { await hookGenerator(appTree, { name: 'use-form', diff --git a/packages/react/src/generators/hook/hook.ts b/packages/react/src/generators/hook/hook.ts index 0240b9d19c19b..d796474c7e870 100644 --- a/packages/react/src/generators/hook/hook.ts +++ b/packages/react/src/generators/hook/hook.ts @@ -7,21 +7,24 @@ import { joinPathFragments, logger, names, - toJS, Tree, } from '@nx/devkit'; - -import { Schema } from './schema'; -import { addImport } from '../../utils/ast-utils'; +import { + determineArtifactNameAndDirectoryOptions, + type FileExtensionType, +} from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; import { join } from 'path'; -import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; +import { addImport } from '../../utils/ast-utils'; +import { Schema } from './schema'; -interface NormalizedSchema extends Schema { +interface NormalizedSchema extends Omit { projectSourceRoot: string; hookName: string; hookTypeName: string; fileName: string; + fileExtension: string; + fileExtensionType: FileExtensionType; projectName: string; directory: string; } @@ -36,25 +39,22 @@ export async function hookGenerator(host: Tree, schema: Schema) { } function createFiles(host: Tree, options: NormalizedSchema) { + const specExt = options.fileExtensionType === 'ts' ? 'tsx' : 'js'; + generateFiles(host, join(__dirname, './files'), options.directory, { ...options, - tmpl: '', + ext: options.fileExtension, + specExt, + isTs: options.fileExtensionType === 'ts', }); - for (const c of host.listChanges()) { - let deleteFile = false; - - if (options.skipTests && /.*spec.ts/.test(c.path)) { - deleteFile = true; - } - - if (deleteFile) { - host.delete(c.path); - } - } - - if (options.js) { - toJS(host); + if (options.skipTests) { + host.delete( + joinPathFragments( + options.directory, + `${options.fileName}.spec.${specExt}` + ) + ); } } @@ -71,25 +71,28 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) { if (options.export && !isApp) { const indexFilePath = joinPathFragments( options.projectSourceRoot, - options.js ? 'index.js' : 'index.ts' + options.fileExtensionType === 'js' ? 'index.js' : 'index.ts' ); - const indexSource = host.read(indexFilePath, 'utf-8'); - if (indexSource !== null) { - const indexSourceFile = tsModule.createSourceFile( - indexFilePath, - indexSource, - tsModule.ScriptTarget.Latest, - true - ); - const changes = applyChangesToString( - indexSource, - addImport( - indexSourceFile, - `export * from './${options.directory}/${options.fileName}';` - ) - ); - host.write(indexFilePath, changes); + + if (!host.exists(indexFilePath)) { + return; } + + const indexSource = host.read(indexFilePath, 'utf-8'); + const indexSourceFile = tsModule.createSourceFile( + indexFilePath, + indexSource, + tsModule.ScriptTarget.Latest, + true + ); + const changes = applyChangesToString( + indexSource, + addImport( + indexSourceFile, + `export * from './${options.directory}/${options.fileName}';` + ) + ); + host.write(indexFilePath, changes); } } @@ -101,11 +104,15 @@ async function normalizeOptions( artifactName, directory, fileName: hookFilename, + fileExtension, + fileExtensionType, project: projectName, } = await determineArtifactNameAndDirectoryOptions(host, { path: options.path, name: options.name, - fileExtension: 'tsx', + allowedFileExtensions: ['js', 'ts'], + fileExtension: options.js ? 'js' : 'ts', + js: options.js, }); const { className } = names(hookFilename); @@ -134,6 +141,8 @@ async function normalizeOptions( hookName, hookTypeName, fileName: hookFilename, + fileExtension, + fileExtensionType, projectSourceRoot, projectName, }; diff --git a/packages/react/src/generators/hook/schema.d.ts b/packages/react/src/generators/hook/schema.d.ts index 79d65055c1a23..226e466765ec5 100644 --- a/packages/react/src/generators/hook/schema.d.ts +++ b/packages/react/src/generators/hook/schema.d.ts @@ -3,5 +3,9 @@ export interface Schema { name?: string; skipTests?: boolean; export?: boolean; + + /** + * @deprecated Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21. + */ js?: boolean; } diff --git a/packages/react/src/generators/hook/schema.json b/packages/react/src/generators/hook/schema.json index 88336f4273791..9bb5d3574a06e 100644 --- a/packages/react/src/generators/hook/schema.json +++ b/packages/react/src/generators/hook/schema.json @@ -8,17 +8,21 @@ "examples": [ { "description": "Generate a hook with the exported symbol matching the file name. It results in the hook `useFoo` at `mylib/src/lib/foo.ts`", - "command": "nx g @nx/react:hook mylib/src/lib/foo" + "command": "nx g @nx/react:hook mylib/src/lib/foo.ts" }, { "description": "Generate a hook with the exported symbol different from the file name. It results in the hook `useCustom` at `mylib/src/lib/foo.ts`", - "command": "nx g @nx/react:hook mylib/src/lib/foo --name=useCustom" + "command": "nx g @nx/react:hook mylib/src/lib/foo.ts --name=useCustom" + }, + { + "description": "Generate a hook without providing the file extension. It results in the hook `useFoo` at `mylib/src/lib/foo.ts`", + "command": "nx g @nx/react:hook mylib/src/lib/foo" } ], "properties": { "path": { "type": "string", - "description": "The file path to the hook without the file extension. Relative to the current working directory.", + "description": "The file path to the hook. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 @@ -33,7 +37,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." }, "skipTests": { "type": "boolean", diff --git a/packages/react/src/generators/redux/files/js/__fileName__.__ext__ b/packages/react/src/generators/redux/files/js/__fileName__.__ext__ new file mode 100644 index 0000000000000..dd683dd937917 --- /dev/null +++ b/packages/react/src/generators/redux/files/js/__fileName__.__ext__ @@ -0,0 +1,114 @@ +import { + createAsyncThunk, + createEntityAdapter, + createSelector, + createSlice, +} from '@reduxjs/toolkit'; + +export const <%= constantName %>_FEATURE_KEY = '<%= propertyName %>'; +export const <%= propertyName %>Adapter = createEntityAdapter(); + +/** + * Export an effect using createAsyncThunk from + * the Redux Toolkit: https://redux-toolkit.js.org/api/createAsyncThunk + * + * e.g. + * ``` + * import React, { useEffect } from 'react'; + * import { useDispatch } from 'react-redux'; + * + * // ... + * + * const dispatch = useDispatch(); + * useEffect(() => { + * dispatch(fetch<%= className %>()) + * }, [dispatch]); + * ``` + */ +export const fetch<%= className %> = createAsyncThunk( + '<%= propertyName %>/fetchStatus', + async (_, thunkAPI) => { + /** + * Replace this with your custom fetch call. + * For example, `return myApi.get<%= className %>s()`; + * Right now we just return an empty array. + */ + return Promise.resolve([]); + } +); + +export const initial<%= className %>State = <%= propertyName %>Adapter.getInitialState({ + loadingStatus: 'not loaded', + error: null +}); + +export const <%= propertyName %>Slice = createSlice({ + name: <%= constantName %>_FEATURE_KEY, + initialState: initial<%= className %>State, + reducers: { + add: <%= propertyName %>Adapter.addOne, + remove: <%= propertyName %>Adapter.removeOne + // ... + }, + extraReducers: builder => { + builder + .addCase(fetch<%= className %>.pending, (state) => { + state.loadingStatus = 'loading'; + }) + .addCase(fetch<%= className %>.fulfilled, (state, action) => { + <%= propertyName %>Adapter.setAll(state, action.payload); + state.loadingStatus = 'loaded'; + }) + .addCase(fetch<%= className %>.rejected, (state, action) => { + state.loadingStatus = 'error'; + state.error = action.error.message; + }); + } +}); + +/* + * Export reducer for store configuration. + */ +export const <%= propertyName %>Reducer = <%= propertyName %>Slice.reducer; + +/* + * Export action creators to be dispatched. For use with the `useDispatch` hook. + * + * e.g. + * ``` + * import React, { useEffect } from 'react'; + * import { useDispatch } from 'react-redux'; + * + * // ... + * + * const dispatch = useDispatch(); + * useEffect(() => { + * dispatch(<%= propertyName %>Actions.add({ id: 1 })) + * }, [dispatch]); + * ``` + * + * See: https://react-redux.js.org/next/api/hooks#usedispatch + */ +export const <%= propertyName %>Actions = <%= propertyName %>Slice.actions; + +/* + * Export selectors to query state. For use with the `useSelector` hook. + * + * e.g. + * ``` + * import { useSelector } from 'react-redux'; + * + * // ... + * + * const entities = useSelector(selectAll<%= className %>); + * ``` + * + * See: https://react-redux.js.org/next/api/hooks#useselector + */ +const { selectAll, selectEntities } = <%= propertyName %>Adapter.getSelectors(); + +export const get<%= className %>State = (rootState) => rootState[<%= constantName %>_FEATURE_KEY]; + +export const selectAll<%= className %> = createSelector(get<%= className %>State, selectAll); + +export const select<%= className %>Entities = createSelector(get<%= className %>State, selectEntities); diff --git a/packages/react/src/generators/redux/files/__fileName__.slice.spec.ts__tmpl__ b/packages/react/src/generators/redux/files/js/__fileName__.spec.__ext__ similarity index 95% rename from packages/react/src/generators/redux/files/__fileName__.slice.spec.ts__tmpl__ rename to packages/react/src/generators/redux/files/js/__fileName__.spec.__ext__ index 5d179a92e0209..053d178b189bd 100644 --- a/packages/react/src/generators/redux/files/__fileName__.slice.spec.ts__tmpl__ +++ b/packages/react/src/generators/redux/files/js/__fileName__.spec.__ext__ @@ -1,4 +1,4 @@ -import { fetch<%= className %>, <%= propertyName %>Adapter, <%= propertyName %>Reducer } from './<%= fileName %>.slice'; +import { fetch<%= className %>, <%= propertyName %>Adapter, <%= propertyName %>Reducer } from './<%= fileName %>'; describe('<%= propertyName %> reducer', () => { it('should handle initial state', () => { diff --git a/packages/react/src/generators/redux/files/__fileName__.slice.ts__tmpl__ b/packages/react/src/generators/redux/files/ts/__fileName__.__ext__ similarity index 100% rename from packages/react/src/generators/redux/files/__fileName__.slice.ts__tmpl__ rename to packages/react/src/generators/redux/files/ts/__fileName__.__ext__ diff --git a/packages/react/src/generators/redux/files/ts/__fileName__.spec.__ext__ b/packages/react/src/generators/redux/files/ts/__fileName__.spec.__ext__ new file mode 100644 index 0000000000000..053d178b189bd --- /dev/null +++ b/packages/react/src/generators/redux/files/ts/__fileName__.spec.__ext__ @@ -0,0 +1,56 @@ +import { fetch<%= className %>, <%= propertyName %>Adapter, <%= propertyName %>Reducer } from './<%= fileName %>'; + +describe('<%= propertyName %> reducer', () => { + it('should handle initial state', () => { + const expected = <%= propertyName %>Adapter.getInitialState({ + loadingStatus: 'not loaded', + error: null + }); + + expect(<%= propertyName %>Reducer(undefined, { type: '' })).toEqual(expected); + }); + + it('should handle fetch<%= className %>', () => { + let state = <%= propertyName %>Reducer( + undefined, + fetch<%= className %>.pending('') + ); + + expect(state).toEqual( + expect.objectContaining({ + loadingStatus: 'loading', + error: null, + entities: {}, + ids: [] + }) + ); + + state = <%= propertyName %>Reducer( + state, + fetch<%= className %>.fulfilled([{ id: 1 }], '') + ); + + expect(state).toEqual( + expect.objectContaining({ + loadingStatus: 'loaded', + error: null, + entities: { 1: { id: 1 } }, + ids: [1] + }) + ); + + state = <%= propertyName %>Reducer( + state, + fetch<%= className %>.rejected(new Error('Uh oh'), '') + ); + + expect(state).toEqual( + expect.objectContaining({ + loadingStatus: 'error', + error: 'Uh oh', + entities: { 1: { id: 1 } }, + ids: [1] + }) + ); + }); +}); diff --git a/packages/react/src/generators/redux/redux.spec.ts b/packages/react/src/generators/redux/redux.spec.ts index cadeef50a39ea..e2a61f70e97d5 100644 --- a/packages/react/src/generators/redux/redux.spec.ts +++ b/packages/react/src/generators/redux/redux.spec.ts @@ -45,6 +45,17 @@ describe('redux', () => { ).toBeTruthy(); }); + it('should handle path with file extension', async () => { + await reduxGenerator(appTree, { + path: 'my-lib/src/lib/my-slice.slice.ts', + }); + + expect(appTree.exists('/my-lib/src/lib/my-slice.slice.ts')).toBeTruthy(); + expect( + appTree.exists('/my-lib/src/lib/my-slice.slice.spec.ts') + ).toBeTruthy(); + }); + describe('--appProject', () => { it('should configure app main', async () => { await applicationGenerator(appTree, { diff --git a/packages/react/src/generators/redux/redux.ts b/packages/react/src/generators/redux/redux.ts index 98634f2960044..ee0cf7e44e15e 100644 --- a/packages/react/src/generators/redux/redux.ts +++ b/packages/react/src/generators/redux/redux.ts @@ -15,7 +15,6 @@ import { joinPathFragments, names, readJson, - toJS, Tree, } from '@nx/devkit'; import { getRootTsConfigPathInTree } from '@nx/js'; @@ -40,17 +39,13 @@ export async function reduxGenerator(host: Tree, schema: Schema) { function generateReduxFiles(host: Tree, options: NormalizedSchema) { generateFiles( host, - joinPathFragments(__dirname, './files'), + joinPathFragments(__dirname, 'files', options.fileExtensionType), options.projectDirectory, { ...options, - tmpl: '', + ext: options.fileExtension, } ); - - if (options.js) { - toJS(host); - } } function addReduxPackageDependencies(host: Tree) { @@ -69,29 +64,31 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) { tsModule = ensureTypescript(); } - const indexFilePath = path.join( + const indexFilePath = joinPathFragments( options.projectSourcePath, - options.js ? 'index.js' : 'index.ts' + options.fileExtensionType === 'js' ? 'index.js' : 'index.ts' ); + if (!host.exists(indexFilePath)) { + return; + } + const indexSource = host.read(indexFilePath, 'utf-8'); - if (indexSource !== null) { - const indexSourceFile = tsModule.createSourceFile( - indexFilePath, - indexSource, - tsModule.ScriptTarget.Latest, - true - ); + const indexSourceFile = tsModule.createSourceFile( + indexFilePath, + indexSource, + tsModule.ScriptTarget.Latest, + true + ); - const statePath = options.path - ? `./lib/${options.path}/${options.fileName}` - : `./lib/${options.fileName}`; - const changes = applyChangesToString( - indexSource, - addImport(indexSourceFile, `export * from '${statePath}.slice';`) - ); - host.write(indexFilePath, changes); - } + const statePath = options.path + ? `./lib/${options.path}/${options.fileName}` + : `./lib/${options.fileName}`; + const changes = applyChangesToString( + indexSource, + addImport(indexSourceFile, `export * from '${statePath}.slice';`) + ); + host.write(indexFilePath, changes); } function addStoreConfiguration(host: Tree, options: NormalizedSchema) { @@ -150,11 +147,16 @@ async function normalizeOptions( artifactName: name, directory, fileName, + fileExtension, + fileExtensionType, project: projectName, } = await determineArtifactNameAndDirectoryOptions(host, { path: options.path, name: options.name, - fileExtension: 'tsx', + suffix: 'slice', + allowedFileExtensions: ['js', 'ts'], + fileExtension: options.js ? 'js' : 'ts', + js: options.js, }); let appProjectSourcePath: string; @@ -206,6 +208,8 @@ async function normalizeOptions( ...options, ...extraNames, fileName, + fileExtension, + fileExtensionType, constantName: names(name).constantName.toUpperCase(), projectDirectory: directory, projectType, diff --git a/packages/react/src/generators/redux/schema.d.ts b/packages/react/src/generators/redux/schema.d.ts index cfaf654d2ff5b..52ea87b81e787 100644 --- a/packages/react/src/generators/redux/schema.d.ts +++ b/packages/react/src/generators/redux/schema.d.ts @@ -1,11 +1,17 @@ +import type { FileExtensionType } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + export interface Schema { path: string; name?: string; appProject?: string; - js?: string; + + /** + * @deprecated Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21. + */ + js?: boolean; } -interface NormalizedSchema extends Schema { +interface NormalizedSchema extends Omit { projectType: string; projectDirectory: string; projectSourcePath: string; @@ -16,4 +22,6 @@ interface NormalizedSchema extends Schema { constantName: string; propertyName: string; fileName: string; + fileExtension: string; + fileExtensionType: FileExtensionType; } diff --git a/packages/react/src/generators/redux/schema.json b/packages/react/src/generators/redux/schema.json index 034548d79004e..ceee60f8deaed 100644 --- a/packages/react/src/generators/redux/schema.json +++ b/packages/react/src/generators/redux/schema.json @@ -8,17 +8,21 @@ "examples": [ { "description": "Generate a Redux state slice with the exported symbol matching the file name. It results in the slice `fooSlice` at `mylib/src/lib/foo.slice.ts`", - "command": "nx g @nx/react:redux mylib/src/lib/foo" + "command": "nx g @nx/react:redux mylib/src/lib/foo.slice.ts" }, { "description": "Generate a Redux state slice with the exported symbol different from the file name. It results in the slice `customSlice` at `mylib/src/lib/foo.slice.ts`", - "command": "nx g @nx/react:redux mylib/src/lib/foo --name=custom" + "command": "nx g @nx/react:redux mylib/src/lib/foo.slice.ts --name=custom" + }, + { + "description": "Generate a Redux state slice without providing the \"slice\" suffix and the file extension. It results in the slice `fooSlice` at `mylib/src/lib/foo.slice.ts`", + "command": "nx g @nx/react:redux mylib/src/lib/foo" } ], "properties": { "path": { "type": "string", - "description": "The file path to the Redux state slice without the file extension. Relative to the current working directory.", + "description": "The file path to the Redux state slice. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 @@ -38,7 +42,7 @@ "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", - "default": false + "x-deprecated": "Provide the full file path including the file extension in the `path` option. This option will be removed in Nx v21." } }, "required": ["path"] diff --git a/packages/remix/src/generators/library/library.impl.spec.ts b/packages/remix/src/generators/library/library.impl.spec.ts index d52397d20cc6f..655e546bc12ad 100644 --- a/packages/remix/src/generators/library/library.impl.spec.ts +++ b/packages/remix/src/generators/library/library.impl.spec.ts @@ -26,7 +26,7 @@ describe('Remix Library Generator', () => { // ASSERT const tsconfig = readJson(tree, 'tsconfig.base.json'); expect(tree.exists(`test/src/server.ts`)); - expect(tree.children(`test/src/lib`)).toMatchSnapshot(); + expect(tree.children(`test/src/lib`).sort()).toMatchSnapshot(); expect(tsconfig.compilerOptions.paths).toMatchSnapshot(); }, 25_000); diff --git a/packages/remix/src/generators/resource-route/resource-route.impl.ts b/packages/remix/src/generators/resource-route/resource-route.impl.ts index dc0f1fb6cc8e6..557d6a87fd791 100644 --- a/packages/remix/src/generators/resource-route/resource-route.impl.ts +++ b/packages/remix/src/generators/resource-route/resource-route.impl.ts @@ -1,17 +1,16 @@ -import { formatFiles, joinPathFragments, Tree } from '@nx/devkit'; +import { formatFiles, Tree } from '@nx/devkit'; import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; -import { - checkRoutePathForErrors, - resolveRemixRouteFile, -} from '../../utils/remix-route-utils'; +import { checkRoutePathForErrors } from '../../utils/remix-route-utils'; import actionGenerator from '../action/action.impl'; import loaderGenerator from '../loader/loader.impl'; import { RemixRouteSchema } from './schema'; export default async function (tree: Tree, options: RemixRouteSchema) { - const { artifactName: name, directory } = + const { filePath: routeFilePath } = await determineArtifactNameAndDirectoryOptions(tree, { path: options.path.replace(/^\//, '').replace(/\/$/, ''), + allowedFileExtensions: ['ts', 'tsx'], + fileExtension: 'tsx', }); if (!options.skipChecks && checkRoutePathForErrors(options.path)) { @@ -20,12 +19,6 @@ export default async function (tree: Tree, options: RemixRouteSchema) { ); } - const routeFilePath = await resolveRemixRouteFile( - tree, - joinPathFragments(directory, name), - undefined - ); - if (tree.exists(routeFilePath)) throw new Error(`Path already exists: ${options.path}`); diff --git a/packages/remix/src/generators/resource-route/schema.json b/packages/remix/src/generators/resource-route/schema.json index 0d5f86c8c8e91..31ce7d829f457 100644 --- a/packages/remix/src/generators/resource-route/schema.json +++ b/packages/remix/src/generators/resource-route/schema.json @@ -6,19 +6,23 @@ "description": "Generate a resource route.", "examples": [ { - "command": "g resource-route 'path/to/page'", - "description": "Generate resource route at /path/to/page" + "description": "Generate a resource route at `myapp/app/routes/foo.ts`", + "command": "nx g resource-route myapp/app/routes/foo.ts" + }, + { + "description": "Generate a resource route without providing the file extension at `myapp/app/routes/foo.tsx`", + "command": "nx g resource-route myapp/app/routes/foo" } ], "properties": { "path": { "type": "string", - "description": "The route path or path to the filename of the route.", + "description": "The file path to the route. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, - "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')" + "x-prompt": "What is the route file path?" }, "action": { "type": "boolean", diff --git a/packages/remix/src/generators/route/route.impl.ts b/packages/remix/src/generators/route/route.impl.ts index f5a5cb6bbbae8..2a59a31a102f2 100644 --- a/packages/remix/src/generators/route/route.impl.ts +++ b/packages/remix/src/generators/route/route.impl.ts @@ -1,17 +1,7 @@ -import { - formatFiles, - joinPathFragments, - names, - readProjectConfiguration, - stripIndents, - Tree, -} from '@nx/devkit'; +import { formatFiles, names, stripIndents, Tree } from '@nx/devkit'; import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { basename, dirname } from 'path'; -import { - checkRoutePathForErrors, - resolveRemixRouteFile, -} from '../../utils/remix-route-utils'; +import { checkRoutePathForErrors } from '../../utils/remix-route-utils'; import ActionGenerator from '../action/action.impl'; import LoaderGenerator from '../loader/loader.impl'; import MetaGenerator from '../meta/meta.impl'; @@ -19,16 +9,12 @@ import StyleGenerator from '../style/style.impl'; import { RemixRouteSchema } from './schema'; export default async function (tree: Tree, options: RemixRouteSchema) { - const { - artifactName: name, - directory, - project: projectName, - } = await determineArtifactNameAndDirectoryOptions(tree, { - path: options.path.replace(/^\//, '').replace(/\/$/, ''), - }); - - const project = readProjectConfiguration(tree, projectName); - if (!project) throw new Error(`Project does not exist: ${projectName}`); + const { artifactName: name, filePath: routeFilePath } = + await determineArtifactNameAndDirectoryOptions(tree, { + path: options.path.replace(/^\//, '').replace(/\/$/, ''), + allowedFileExtensions: ['ts', 'tsx'], + fileExtension: 'tsx', + }); if (!options.skipChecks && checkRoutePathForErrors(options.path)) { throw new Error( @@ -36,18 +22,8 @@ export default async function (tree: Tree, options: RemixRouteSchema) { ); } - const routeFilePath = await resolveRemixRouteFile( - tree, - joinPathFragments(directory, name), - undefined - ); - - const nameToUseForComponent = name.replace('.tsx', ''); - const { className: componentName } = names( - nameToUseForComponent === '.' || nameToUseForComponent === '' - ? basename(dirname(routeFilePath)) - : nameToUseForComponent + name === '.' || name === '' ? basename(dirname(routeFilePath)) : name ); if (tree.exists(routeFilePath)) diff --git a/packages/remix/src/generators/route/schema.json b/packages/remix/src/generators/route/schema.json index 93a80f466efa1..662184420d25e 100644 --- a/packages/remix/src/generators/route/schema.json +++ b/packages/remix/src/generators/route/schema.json @@ -6,19 +6,23 @@ "type": "object", "examples": [ { - "command": "g route 'path/to/page'", - "description": "Generate route at /path/to/page" + "description": "Generate a route at `myapp/app/routes/foo.tsx`", + "command": "nx g resource-route myapp/app/routes/foo.tsx" + }, + { + "description": "Generate a route without providing the file extension at `myapp/app/routes/foo.tsx`", + "command": "nx g resource-route myapp/app/routes/foo" } ], "properties": { "path": { "type": "string", - "description": "The route path or path to the filename of the route.", + "description": "The file path to the route. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, - "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar')" + "x-prompt": "What is the route file path?" }, "style": { "type": "string", diff --git a/packages/remix/src/generators/style/schema.json b/packages/remix/src/generators/style/schema.json index de0b75768a007..ad4f6ff207603 100644 --- a/packages/remix/src/generators/style/schema.json +++ b/packages/remix/src/generators/style/schema.json @@ -6,19 +6,19 @@ "type": "object", "examples": [ { - "command": "g style --path='apps/demo/app/routes/path/to/page.tsx'", - "description": "Generate route at apps/demo/app/routes/path/to/page.tsx" + "description": "Generate a stylesheet at `myapp/app/styles/foo.css`", + "command": "nx g style myapp/app/routes/foo.tsx" } ], "properties": { "path": { "type": "string", - "description": "Route path", + "description": "The file path to the route. Relative to the current working directory.", "$default": { "$source": "argv", "index": 0 }, - "x-prompt": "What is the path of the route? (e.g. 'apps/demo/app/routes/foo/bar.tsx')" + "x-prompt": "What is the route file path?" } }, "required": ["path"] diff --git a/packages/remix/src/generators/style/style.impl.ts b/packages/remix/src/generators/style/style.impl.ts index 88360f0b3b071..7fcb6381d509d 100644 --- a/packages/remix/src/generators/style/style.impl.ts +++ b/packages/remix/src/generators/style/style.impl.ts @@ -17,17 +17,20 @@ import { } from '../../utils/remix-route-utils'; export default async function (tree: Tree, options: RemixStyleSchema) { - const { project: projectName, artifactName: name } = - await determineArtifactNameAndDirectoryOptions(tree, { - path: options.path, - }); + const { + project: projectName, + directory, + fileName, + } = await determineArtifactNameAndDirectoryOptions(tree, { + path: options.path, + }); const project = readProjectConfiguration(tree, projectName); if (!project) throw new Error(`Project does not exist: ${projectName}`); const appDir = await resolveRemixAppDirectory(tree, project.name); - const normalizedRoutePath = `${normalizeRoutePath(options.path) - .replace(/^\//, '') - .replace('.tsx', '')}.css`; + const normalizedRoutePath = `${normalizeRoutePath( + joinPathFragments(directory, fileName) + ).replace(/^\//, '')}.css`; const stylesheetPath = joinPathFragments( appDir, 'styles', diff --git a/packages/remix/src/utils/remix-route-utils.ts b/packages/remix/src/utils/remix-route-utils.ts index ac4ba4c9c89e3..c27d73816c606 100644 --- a/packages/remix/src/utils/remix-route-utils.ts +++ b/packages/remix/src/utils/remix-route-utils.ts @@ -1,75 +1,6 @@ -import { - joinPathFragments, - names, - readProjectConfiguration, - Tree, -} from '@nx/devkit'; -import { getRemixConfigValues } from './remix-config'; +import { joinPathFragments, readProjectConfiguration, Tree } from '@nx/devkit'; import { relative } from 'path'; - -/** - * - * @param tree - * @param path to the route which could be fully specified or just "foo/bar" - * @param projectName the name of the project where the route should be added - * @param fileExtension the file extension to add to resolved route file - * @returns file path to the route - */ -export async function resolveRemixRouteFile( - tree: Tree, - path: string, - projectName?: string, - fileExtension?: string -): Promise { - const { name: routePath } = names(path.replace(/^\//, '').replace(/\/$/, '')); - - if (!projectName) { - return appendRouteFileExtension(tree, routePath, fileExtension); - } else { - const project = readProjectConfiguration(tree, projectName); - if (!project) throw new Error(`Project does not exist: ${projectName}`); - const normalizedRoutePath = normalizeRoutePath(routePath); - const fileName = appendRouteFileExtension( - tree, - normalizedRoutePath, - fileExtension - ); - - return joinPathFragments( - await resolveRemixAppDirectory(tree, projectName), - 'routes', - fileName - ); - } -} - -function appendRouteFileExtension( - tree: Tree, - routePath: string, - fileExtension?: string -) { - // if no file extension specified, let's try to find it - if (!fileExtension) { - // see if the path already has it - const extensionMatch = routePath.match(/(\.[^.]+)$/); - - if (extensionMatch) { - fileExtension = extensionMatch[0]; - } else { - // look for either .ts or .tsx to exist in tree - if (tree.exists(`${routePath}.ts`)) { - fileExtension = '.ts'; - } else { - // default to .tsx if nothing else found - fileExtension = '.tsx'; - } - } - } - - return routePath.endsWith(fileExtension) - ? routePath - : `${routePath}${fileExtension}`; -} +import { getRemixConfigValues } from './remix-config'; export function normalizeRoutePath(path: string) { return path.indexOf('/routes/') > -1 diff --git a/packages/vue/src/generators/component/component.spec.ts b/packages/vue/src/generators/component/component.spec.ts index e023e6f6e2250..93e0360b5e904 100644 --- a/packages/vue/src/generators/component/component.spec.ts +++ b/packages/vue/src/generators/component/component.spec.ts @@ -55,6 +55,39 @@ describe('component', () => { `); }); + it('should handle path with file extension', async () => { + await componentGenerator(appTree, { + path: `${libName}/src/lib/hello/hello.vue`, + }); + + expect(appTree.read(`${libName}/src/lib/hello/hello.vue`, 'utf-8')) + .toMatchInlineSnapshot(` + " + + + + + " + `); + expect(appTree.read(`${libName}/src/lib/hello/hello.spec.ts`, 'utf-8')) + .toMatchInlineSnapshot(` + "import { mount } from '@vue/test-utils'; + import Hello from './hello.vue'; + + describe('Hello', () => { + it('renders properly', () => { + const wrapper = mount(Hello, {}); + expect(wrapper.text()).toContain('Welcome to Hello'); + }); + }); + " + `); + }); + it('should have correct component name based on directory', async () => { await componentGenerator(appTree, { path: `${libName}/src/foo/bar/hello-world/hello-world`, diff --git a/packages/vue/src/generators/component/component.ts b/packages/vue/src/generators/component/component.ts index 843d6636b8fdc..712de89e70d1e 100644 --- a/packages/vue/src/generators/component/component.ts +++ b/packages/vue/src/generators/component/component.ts @@ -1,4 +1,9 @@ -import { formatFiles, generateFiles, toJS, Tree } from '@nx/devkit'; +import { + formatFiles, + generateFiles, + joinPathFragments, + Tree, +} from '@nx/devkit'; import { join } from 'path'; import { addExportsToBarrel, normalizeOptions } from './lib/utils'; import { NormalizedSchema, ComponentGeneratorSchema } from './schema'; @@ -18,27 +23,22 @@ export async function componentGenerator( } function createComponentFiles(host: Tree, options: NormalizedSchema) { + const specExt = options.js ? 'js' : 'ts'; generateFiles(host, join(__dirname, './files'), options.directory, { ...options, + isTs: !options.js, + specExt, tmpl: '', }); - for (const c of host.listChanges()) { - let deleteFile = false; - - if ( - (options.skipTests || options.inSourceTests) && - /.*spec.ts/.test(c.path) - ) { - deleteFile = true; - } - - if (deleteFile) { - host.delete(c.path); - } + if (options.skipTests || options.inSourceTests) { + host.delete( + joinPathFragments( + options.directory, + `${options.fileName}.spec.${specExt}` + ) + ); } - - if (options.js) toJS(host); } export default componentGenerator; diff --git a/packages/vue/src/generators/component/files/__fileName__.spec.ts__tmpl__ b/packages/vue/src/generators/component/files/__fileName__.spec.__specExt__ similarity index 100% rename from packages/vue/src/generators/component/files/__fileName__.spec.ts__tmpl__ rename to packages/vue/src/generators/component/files/__fileName__.spec.__specExt__ diff --git a/packages/vue/src/generators/component/files/__fileName__.vue__tmpl__ b/packages/vue/src/generators/component/files/__fileName__.vue__tmpl__ index b9319e12c9f1a..da5196a388c72 100644 --- a/packages/vue/src/generators/component/files/__fileName__.vue__tmpl__ +++ b/packages/vue/src/generators/component/files/__fileName__.vue__tmpl__ @@ -1,6 +1,12 @@ +<%_ if (isTs) { _%> +<%_ } else { _%> + +<%_ } _%>