feat(misc): handle artifact generators' path options including file extensions (#29111)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

Artifact generators don't handle consistently receiving a file extension
in the `path` option.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

Artifact generators should handle receiving a file extension in the
`path` option. If the file extension is passed, the file path will be
treated as "complete" and used fully as provided. If the `path` provided
doesn't contain a file extension, the default extension will be appended
to it (or the one provided in a related option, e.g. `--language`,
`--js`, etc) together with the suffix for generators that use it.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2024-12-09 15:13:15 +01:00 committed by GitHub
parent 3474d7c607
commit 28c53f942b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
156 changed files with 1794 additions and 660 deletions

View File

@ -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"],

View File

@ -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?"
},

View File

@ -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?"
},

View File

@ -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?"
},

View File

@ -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?"
},

View File

@ -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?"
},

View File

@ -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.",

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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?"

View File

@ -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.",

View File

@ -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?"

View File

@ -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.",

View File

@ -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"

View File

@ -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"

View File

@ -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"
},
{

View File

@ -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",

View File

@ -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.",

View File

@ -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",

View File

@ -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"],

View File

@ -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",

View File

@ -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",

View File

@ -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"],

View File

@ -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?"
},

View File

@ -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
```

View File

@ -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<ExampleComponentComponent>;
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`] = `
"<p>example.component works!</p>
"
`;
exports[`component Generator should inline styles when --inline-style=true 1`] = `
"import { Component } from '@angular/core';

View File

@ -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' });

View File

@ -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);

View File

@ -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

View File

@ -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();
});
});
"
`;

View File

@ -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);

View File

@ -20,6 +20,8 @@ export async function normalizeOptions(
name: options.name,
path: options.path,
suffix: 'directive',
allowedFileExtensions: ['ts'],
fileExtension: 'ts',
});
const { className } = names(name);

View File

@ -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

View File

@ -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();
});
});
"
`;

View File

@ -18,6 +18,8 @@ export async function normalizeOptions(
name: options.name,
path: options.path,
suffix: 'pipe',
allowedFileExtensions: ['ts'],
fileExtension: 'ts',
});
const { className } = names(name);

View File

@ -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);

View File

@ -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

View File

@ -18,6 +18,8 @@ export async function normalizeOptions(
name: options.name,
path: options.path,
suffix: 'directive',
allowedFileExtensions: ['ts'],
fileExtension: 'ts',
});
const { className } = names(name);

View File

@ -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' });

View File

@ -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

View File

@ -18,6 +18,8 @@ export async function normalizeOptions(
name: options.name,
path: options.path,
suffix: 'pipe',
allowedFileExtensions: ['ts'],
fileExtension: 'ts',
});
const { className } = names(name);

View File

@ -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' });

View File

@ -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

View File

@ -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);

View File

@ -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' });

View File

@ -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

View File

@ -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,31 @@ 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',
});
setCwd('apps/app1');
const result = await determineArtifactNameAndDirectoryOptions(tree, {
path: 'myComponent',
});
expect(result).toStrictEqual({
artifactName: 'myComponent',
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', async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
@ -60,53 +84,14 @@ 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 () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
});
setCwd('apps/app1');
const result = await determineArtifactNameAndDirectoryOptions(tree, {
path: 'apps/app1/myComponent',
});
expect(result).toStrictEqual({
artifactName: 'myComponent',
directory: 'apps/app1',
fileName: 'myComponent',
filePath: 'apps/app1/myComponent.ts',
project: 'app1',
});
restoreCwd();
});
it('should return the options as provided when directory is provided', async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
});
const result = await determineArtifactNameAndDirectoryOptions(tree, {
path: 'apps/app1/myComponent',
});
expect(result).toStrictEqual({
artifactName: 'myComponent',
directory: 'apps/app1',
fileName: 'myComponent',
filePath: 'apps/app1/myComponent.ts',
project: 'app1',
});
});
it(`should handle window's style paths correctly`, async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
@ -122,25 +107,8 @@ describe('determineArtifactNameAndDirectoryOptions', () => {
directory: 'apps/app1',
fileName: 'myComponent',
filePath: 'apps/app1/myComponent.ts',
project: 'app1',
});
});
it('should support receiving a path as the name', async () => {
addProjectConfiguration(tree, 'app1', {
root: 'apps/app1',
projectType: 'application',
});
const result = await determineArtifactNameAndDirectoryOptions(tree, {
path: 'apps/app1/foo/bar/myComponent',
});
expect(result).toStrictEqual({
artifactName: 'myComponent',
directory: 'apps/app1/foo/bar',
fileName: 'myComponent',
filePath: 'apps/app1/foo/bar/myComponent.ts',
fileExtension: 'ts',
fileExtensionType: 'ts',
project: 'app1',
});
});
@ -161,26 +129,51 @@ describe('determineArtifactNameAndDirectoryOptions', () => {
directory: 'apps/app1',
fileName: 'myComponent.component',
filePath: 'apps/app1/myComponent.component.ts',
fileExtension: 'ts',
fileExtensionType: 'ts',
project: 'app1',
});
});
it('should support receiving a fileName', 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, {
fileName: 'myComponent.component',
path: 'apps/app1/myComponent',
path: 'apps/app1/myComponent.ts',
});
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 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',
});
expect(result).toStrictEqual({
artifactName: 'myComponent',
directory: 'apps/app1',
fileName: 'myComponent',
filePath: 'apps/app1/myComponent.ts',
fileExtension: 'ts',
fileExtensionType: 'ts',
project: 'app1',
});
});
@ -201,7 +194,97 @@ describe('determineArtifactNameAndDirectoryOptions', () => {
directory: 'apps/app1',
fileName: 'myComponent',
filePath: 'apps/app1/myComponent.tsx',
fileExtension: 'tsx',
fileExtensionType: 'ts',
project: 'app1',
});
});
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, {
path: 'apps/app1/myComponent.astro',
allowedFileExtensions: ['astro'],
});
expect(result).toStrictEqual({
artifactName: 'myComponent',
directory: 'apps/app1',
fileName: 'myComponent',
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."`
);
});
});

View File

@ -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;
}

View File

@ -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,

View File

@ -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, {
generateFiles(
host,
join(__dirname, './files', options.fileExtensionType),
options.directory,
{
...options,
tmpl: '',
});
for (const c of host.listChanges()) {
let deleteFile = false;
if (options.skipTests && /.*spec.tsx/.test(c.path)) {
deleteFile = true;
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,10 +52,14 @@ 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'
);
if (!host.exists(indexFilePath)) {
return;
}
const indexSource = host.read(indexFilePath, 'utf-8');
if (indexSource !== null) {
const indexSourceFile = ts.createSourceFile(
indexFilePath,
indexSource,
@ -75,7 +76,6 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) {
);
host.write(indexFilePath, changes);
}
}
}
function getRelativeImportToFile(indexPath: string, filePath: string) {

View File

@ -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 (
<View>
<Text>Welcome to <%= name %>!</Text>
</View>
);
}
}
<%_ } else { _%>
export function <%= className %>(props) {
return (
<View>
<Text>Welcome to <%= name %>!</Text>
</View>
);
}
<%_ } _%>
export default <%= className %>;

View File

@ -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();
});
});

View File

@ -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 (
<View>
@ -27,6 +27,6 @@ export function <%= className %>(props: <%= className %>Props) {
</View>
);
}
<% } %>
<%_ } _%>
export default <%= className %>;

View File

@ -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();
});
});

View File

@ -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<Schema, 'js'> {
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,

View File

@ -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;
}

View File

@ -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.",

View File

@ -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",

View File

@ -31,7 +31,9 @@ async function normalizeControllerOptions(
tree: Tree,
options: ControllerGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'controller',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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",

View File

@ -22,7 +22,9 @@ async function normalizeDecoratorOptions(
tree: Tree,
options: DecoratorGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'decorator',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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",

View File

@ -28,7 +28,9 @@ async function normalizeFilterOptions(
tree: Tree,
options: FilterGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'filter',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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",

View File

@ -28,7 +28,9 @@ async function normalizeGatewayOptions(
tree: Tree,
options: GatewayGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'gateway',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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",

View File

@ -28,7 +28,9 @@ async function normalizeGuardOptions(
tree: Tree,
options: GuardGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'guard',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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",

View File

@ -28,7 +28,9 @@ async function normalizeInterceptorOptions(
tree: Tree,
options: InterceptorGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'interceptor',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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",

View File

@ -8,7 +8,11 @@ export async function interfaceGenerator(
tree: Tree,
rawOptions: InterfaceGeneratorOptions
): Promise<any> {
const options = await normalizeOptions(tree, rawOptions);
const options = await normalizeOptions(tree, rawOptions, {
allowedFileExtensions: ['ts'],
skipLanguageOption: true,
suffix: 'interface',
});
return runNestSchematic(tree, 'interface', options);
}

View File

@ -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",

View File

@ -28,7 +28,9 @@ async function normalizeMiddlewareOptions(
tree: Tree,
options: MiddlewareGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'middleware',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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",

View File

@ -25,7 +25,9 @@ async function normalizeModuleOptions(
tree: Tree,
options: ModuleGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOption = await normalizeOptions(tree, options);
const normalizedOption = await normalizeOptions(tree, options, {
suffix: 'module',
});
return {
...normalizedOption,
language: options.language,

View File

@ -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",

View File

@ -28,7 +28,9 @@ async function normalizePipeOptions(
tree: Tree,
options: PipeGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'pipe',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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",

View File

@ -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",

View File

@ -28,7 +28,9 @@ async function normalizeResolverOptions(
tree: Tree,
options: ResolverGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'resolver',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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",

View File

@ -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<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
skipLanguageOption: true,
});
return {
...normalizedOptions,
language: options.language,
spec: unitTestRunnerToSpec(options.unitTestRunner),
};
}

View File

@ -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.",

View File

@ -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",

View File

@ -28,7 +28,9 @@ async function normalizeServiceOptions(
tree: Tree,
options: ServiceGeneratorOptions
): Promise<NormalizedOptions> {
const normalizedOptions = await normalizeOptions(tree, options);
const normalizedOptions = await normalizeOptions(tree, options, {
suffix: 'service',
});
return {
...normalizedOptions,
language: options.language,

View File

@ -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<NormalizedOptions> {
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,
};
}

View File

@ -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 %}

View File

@ -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',

View File

@ -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,
// 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,
fileExtension: 'tsx',
});
const componentInstall = await reactComponentGenerator(host, {
...options,
name,
classComponent: false,
routing: false,
skipFormat: true,

View File

@ -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.",

Some files were not shown because too many files have changed in this diff Show More