feat(core): initial implementation of nx release (#19110)
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
This commit is contained in:
parent
11fcb8f2d4
commit
9116c29c18
@ -115,7 +115,7 @@ Type: `boolean`
|
|||||||
|
|
||||||
Show help
|
Show help
|
||||||
|
|
||||||
### nx-bail
|
### nxBail
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ Default: `false`
|
|||||||
|
|
||||||
Stop command execution after the first failed task
|
Stop command execution after the first failed task
|
||||||
|
|
||||||
### nx-ignore-cycles
|
### nxIgnoreCycles
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -151,7 +151,7 @@ Type: `string`
|
|||||||
|
|
||||||
This is the name of the tasks runner configured in nx.json
|
This is the name of the tasks runner configured in nx.json
|
||||||
|
|
||||||
### skip-nx-cache
|
### skipNxCache
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ Type: `string`
|
|||||||
|
|
||||||
Show the task graph of the command. Pass a file path to save the graph data instead of viewing it in the browser.
|
Show the task graph of the command. Pass a file path to save the graph data instead of viewing it in the browser.
|
||||||
|
|
||||||
### nx-bail
|
### nxBail
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ Default: `false`
|
|||||||
|
|
||||||
Stop command execution after the first failed task
|
Stop command execution after the first failed task
|
||||||
|
|
||||||
### nx-ignore-cycles
|
### nxIgnoreCycles
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ Type: `string`
|
|||||||
|
|
||||||
This is the name of the tasks runner configured in nx.json
|
This is the name of the tasks runner configured in nx.json
|
||||||
|
|
||||||
### skip-nx-cache
|
### skipNxCache
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
|
|||||||
250
docs/generated/cli/release.md
Normal file
250
docs/generated/cli/release.md
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
---
|
||||||
|
title: 'release - CLI command'
|
||||||
|
description: '**ALPHA**: Orchestrate versioning and publishing of applications and libraries'
|
||||||
|
---
|
||||||
|
|
||||||
|
# release
|
||||||
|
|
||||||
|
**ALPHA**: Orchestrate versioning and publishing of applications and libraries
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nx release
|
||||||
|
```
|
||||||
|
|
||||||
|
Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### dryRun
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Preview the changes without updating files/creating releases
|
||||||
|
|
||||||
|
### groups
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
One or more release groups to target with the current command.
|
||||||
|
|
||||||
|
### help
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show help
|
||||||
|
|
||||||
|
### projects
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Projects to run. (comma/space delimited project names and/or patterns)
|
||||||
|
|
||||||
|
### verbose
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Prints additional information about the commands (e.g., stack traces)
|
||||||
|
|
||||||
|
### version
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show version number
|
||||||
|
|
||||||
|
## Subcommands
|
||||||
|
|
||||||
|
### version
|
||||||
|
|
||||||
|
Create a version and release for one or more applications and libraries
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nx release version [specifier]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
##### help
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show help
|
||||||
|
|
||||||
|
##### preid
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The optional prerelease identifier to apply to the version, in the case that specifier has been set to prerelease.
|
||||||
|
|
||||||
|
##### specifier
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Exact version or semver keyword to apply to the selected release group.
|
||||||
|
|
||||||
|
##### version
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show version number
|
||||||
|
|
||||||
|
### changelog
|
||||||
|
|
||||||
|
Generate a changelog for one or more projects, and optionally push to Github
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nx release changelog [version]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
##### from
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The git reference to use as the start of the changelog. If not set it will attempt to resolve the latest tag and use that
|
||||||
|
|
||||||
|
##### gitRemote
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `origin`
|
||||||
|
|
||||||
|
Alternate git remote in the form {user}/{repo} on which to create the Github release (useful for testing)
|
||||||
|
|
||||||
|
##### help
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show help
|
||||||
|
|
||||||
|
##### interactive
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
##### tagVersionPrefix
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `v`
|
||||||
|
|
||||||
|
Prefix to apply to the version when creating the Github release tag
|
||||||
|
|
||||||
|
##### to
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `HEAD`
|
||||||
|
|
||||||
|
The git reference to use as the end of the changelog
|
||||||
|
|
||||||
|
##### version
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The version to create a Github release and changelog for
|
||||||
|
|
||||||
|
### publish
|
||||||
|
|
||||||
|
Publish a versioned project to a registry
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nx release publish
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
##### all
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
[deprecated] `run-many` runs all targets on all projects in the workspace if no projects are provided. This option is no longer required.
|
||||||
|
|
||||||
|
##### exclude
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Exclude certain projects from being processed
|
||||||
|
|
||||||
|
##### graph
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Show the task graph of the command. Pass a file path to save the graph data instead of viewing it in the browser.
|
||||||
|
|
||||||
|
##### help
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show help
|
||||||
|
|
||||||
|
##### nxBail
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Stop command execution after the first failed task
|
||||||
|
|
||||||
|
##### nxIgnoreCycles
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Ignore cycles in the task graph
|
||||||
|
|
||||||
|
##### parallel
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Max number of parallel processes [default is 3]
|
||||||
|
|
||||||
|
##### projects
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Projects to run. (comma/space delimited project names and/or patterns)
|
||||||
|
|
||||||
|
##### registry
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The registry to publish to
|
||||||
|
|
||||||
|
##### runner
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
This is the name of the tasks runner configured in nx.json
|
||||||
|
|
||||||
|
##### skipNxCache
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Rerun the tasks even when the results are available in the cache
|
||||||
|
|
||||||
|
##### tag
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The distribution tag to apply to the published package
|
||||||
|
|
||||||
|
##### verbose
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Prints additional information about the commands (e.g., stack traces)
|
||||||
|
|
||||||
|
##### version
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show version number
|
||||||
@ -105,7 +105,7 @@ Type: `boolean`
|
|||||||
|
|
||||||
Show help
|
Show help
|
||||||
|
|
||||||
### nx-bail
|
### nxBail
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ Default: `false`
|
|||||||
|
|
||||||
Stop command execution after the first failed task
|
Stop command execution after the first failed task
|
||||||
|
|
||||||
### nx-ignore-cycles
|
### nxIgnoreCycles
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ Type: `string`
|
|||||||
|
|
||||||
This is the name of the tasks runner configured in nx.json
|
This is the name of the tasks runner configured in nx.json
|
||||||
|
|
||||||
### skip-nx-cache
|
### skipNxCache
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ Nx.json configuration
|
|||||||
- [npmScope](../../devkit/documents/NxJsonConfiguration#npmscope): string
|
- [npmScope](../../devkit/documents/NxJsonConfiguration#npmscope): string
|
||||||
- [plugins](../../devkit/documents/NxJsonConfiguration#plugins): string[]
|
- [plugins](../../devkit/documents/NxJsonConfiguration#plugins): string[]
|
||||||
- [pluginsConfig](../../devkit/documents/NxJsonConfiguration#pluginsconfig): Record<string, unknown>
|
- [pluginsConfig](../../devkit/documents/NxJsonConfiguration#pluginsconfig): Record<string, unknown>
|
||||||
|
- [release](../../devkit/documents/NxJsonConfiguration#release): NxReleaseConfiguration
|
||||||
- [targetDefaults](../../devkit/documents/NxJsonConfiguration#targetdefaults): TargetDefaults
|
- [targetDefaults](../../devkit/documents/NxJsonConfiguration#targetdefaults): TargetDefaults
|
||||||
- [tasksRunnerOptions](../../devkit/documents/NxJsonConfiguration#tasksrunneroptions): Object
|
- [tasksRunnerOptions](../../devkit/documents/NxJsonConfiguration#tasksrunneroptions): Object
|
||||||
- [workspaceLayout](../../devkit/documents/NxJsonConfiguration#workspacelayout): Object
|
- [workspaceLayout](../../devkit/documents/NxJsonConfiguration#workspacelayout): Object
|
||||||
@ -163,6 +164,14 @@ Configuration for Nx Plugins
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### release
|
||||||
|
|
||||||
|
• `Optional` **release**: `NxReleaseConfiguration`
|
||||||
|
|
||||||
|
**ALPHA**: Configuration for `nx release` (versioning and publishing of applications and libraries)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### targetDefaults
|
### targetDefaults
|
||||||
|
|
||||||
• `Optional` **targetDefaults**: `TargetDefaults`
|
• `Optional` **targetDefaults**: `TargetDefaults`
|
||||||
|
|||||||
@ -28,6 +28,7 @@ use ProjectsConfigurations or NxJsonConfiguration
|
|||||||
- [plugins](../../devkit/documents/Workspace#plugins): string[]
|
- [plugins](../../devkit/documents/Workspace#plugins): string[]
|
||||||
- [pluginsConfig](../../devkit/documents/Workspace#pluginsconfig): Record<string, unknown>
|
- [pluginsConfig](../../devkit/documents/Workspace#pluginsconfig): Record<string, unknown>
|
||||||
- [projects](../../devkit/documents/Workspace#projects): Record<string, ProjectConfiguration>
|
- [projects](../../devkit/documents/Workspace#projects): Record<string, ProjectConfiguration>
|
||||||
|
- [release](../../devkit/documents/Workspace#release): NxReleaseConfiguration
|
||||||
- [targetDefaults](../../devkit/documents/Workspace#targetdefaults): TargetDefaults
|
- [targetDefaults](../../devkit/documents/Workspace#targetdefaults): TargetDefaults
|
||||||
- [tasksRunnerOptions](../../devkit/documents/Workspace#tasksrunneroptions): Object
|
- [tasksRunnerOptions](../../devkit/documents/Workspace#tasksrunneroptions): Object
|
||||||
- [version](../../devkit/documents/Workspace#version): number
|
- [version](../../devkit/documents/Workspace#version): number
|
||||||
@ -219,6 +220,18 @@ Projects' projects
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### release
|
||||||
|
|
||||||
|
• `Optional` **release**: `NxReleaseConfiguration`
|
||||||
|
|
||||||
|
**ALPHA**: Configuration for `nx release` (versioning and publishing of applications and libraries)
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
[NxJsonConfiguration](../../devkit/documents/NxJsonConfiguration).[release](../../devkit/documents/NxJsonConfiguration#release)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### targetDefaults
|
### targetDefaults
|
||||||
|
|
||||||
• `Optional` **targetDefaults**: `TargetDefaults`
|
• `Optional` **targetDefaults**: `TargetDefaults`
|
||||||
|
|||||||
@ -7104,6 +7104,14 @@
|
|||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "release-publish",
|
||||||
|
"path": "/packages/js/executors/release-publish",
|
||||||
|
"name": "release-publish",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "verdaccio",
|
"id": "verdaccio",
|
||||||
"path": "/packages/js/executors/verdaccio",
|
"path": "/packages/js/executors/verdaccio",
|
||||||
@ -7145,6 +7153,14 @@
|
|||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "release-version",
|
||||||
|
"path": "/packages/js/generators/release-version",
|
||||||
|
"name": "release-version",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "setup-verdaccio",
|
"id": "setup-verdaccio",
|
||||||
"path": "/packages/js/generators/setup-verdaccio",
|
"path": "/packages/js/generators/setup-verdaccio",
|
||||||
@ -7824,6 +7840,14 @@
|
|||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"children": [],
|
"children": [],
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"path": "/packages/nx/documents/release",
|
||||||
|
"id": "release",
|
||||||
|
"isExternal": false,
|
||||||
|
"children": [],
|
||||||
|
"disableCollapsible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
|
|||||||
@ -1010,6 +1010,15 @@
|
|||||||
"path": "/packages/js/executors/node",
|
"path": "/packages/js/executors/node",
|
||||||
"type": "executor"
|
"type": "executor"
|
||||||
},
|
},
|
||||||
|
"/packages/js/executors/release-publish": {
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx run`. Use `nx release publish` instead.",
|
||||||
|
"file": "generated/packages/js/executors/release-publish.json",
|
||||||
|
"hidden": true,
|
||||||
|
"name": "release-publish",
|
||||||
|
"originalFilePath": "/packages/js/src/executors/release-publish/schema.json",
|
||||||
|
"path": "/packages/js/executors/release-publish",
|
||||||
|
"type": "executor"
|
||||||
|
},
|
||||||
"/packages/js/executors/verdaccio": {
|
"/packages/js/executors/verdaccio": {
|
||||||
"description": "Start local registry with verdaccio",
|
"description": "Start local registry with verdaccio",
|
||||||
"file": "generated/packages/js/executors/verdaccio.json",
|
"file": "generated/packages/js/executors/verdaccio.json",
|
||||||
@ -1048,6 +1057,15 @@
|
|||||||
"path": "/packages/js/generators/convert-to-swc",
|
"path": "/packages/js/generators/convert-to-swc",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
},
|
},
|
||||||
|
"/packages/js/generators/release-version": {
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx generate`. Use `nx release version` instead.",
|
||||||
|
"file": "generated/packages/js/generators/release-version.json",
|
||||||
|
"hidden": true,
|
||||||
|
"name": "release-version",
|
||||||
|
"originalFilePath": "/packages/js/src/generators/release-version/schema.json",
|
||||||
|
"path": "/packages/js/generators/release-version",
|
||||||
|
"type": "generator"
|
||||||
|
},
|
||||||
"/packages/js/generators/setup-verdaccio": {
|
"/packages/js/generators/setup-verdaccio": {
|
||||||
"description": "Setup Verdaccio for local package management.",
|
"description": "Setup Verdaccio for local package management.",
|
||||||
"file": "generated/packages/js/generators/setup-verdaccio.json",
|
"file": "generated/packages/js/generators/setup-verdaccio.json",
|
||||||
@ -1773,6 +1791,17 @@
|
|||||||
"path": "/packages/nx/documents/view-logs",
|
"path": "/packages/nx/documents/view-logs",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"originalFilePath": "generated/cli/view-logs"
|
"originalFilePath": "generated/cli/view-logs"
|
||||||
|
},
|
||||||
|
"/packages/nx/documents/release": {
|
||||||
|
"id": "release",
|
||||||
|
"name": "release",
|
||||||
|
"description": "The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.",
|
||||||
|
"file": "generated/packages/nx/documents/release",
|
||||||
|
"itemList": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"path": "/packages/nx/documents/release",
|
||||||
|
"tags": [],
|
||||||
|
"originalFilePath": "generated/cli/release"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "/packages/nx",
|
"root": "/packages/nx",
|
||||||
|
|||||||
@ -994,6 +994,15 @@
|
|||||||
"path": "js/executors/node",
|
"path": "js/executors/node",
|
||||||
"type": "executor"
|
"type": "executor"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx run`. Use `nx release publish` instead.",
|
||||||
|
"file": "generated/packages/js/executors/release-publish.json",
|
||||||
|
"hidden": true,
|
||||||
|
"name": "release-publish",
|
||||||
|
"originalFilePath": "/packages/js/src/executors/release-publish/schema.json",
|
||||||
|
"path": "js/executors/release-publish",
|
||||||
|
"type": "executor"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Start local registry with verdaccio",
|
"description": "Start local registry with verdaccio",
|
||||||
"file": "generated/packages/js/executors/verdaccio.json",
|
"file": "generated/packages/js/executors/verdaccio.json",
|
||||||
@ -1032,6 +1041,15 @@
|
|||||||
"path": "js/generators/convert-to-swc",
|
"path": "js/generators/convert-to-swc",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx generate`. Use `nx release version` instead.",
|
||||||
|
"file": "generated/packages/js/generators/release-version.json",
|
||||||
|
"hidden": true,
|
||||||
|
"name": "release-version",
|
||||||
|
"originalFilePath": "/packages/js/src/generators/release-version/schema.json",
|
||||||
|
"path": "js/generators/release-version",
|
||||||
|
"type": "generator"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Setup Verdaccio for local package management.",
|
"description": "Setup Verdaccio for local package management.",
|
||||||
"file": "generated/packages/js/generators/setup-verdaccio.json",
|
"file": "generated/packages/js/generators/setup-verdaccio.json",
|
||||||
@ -1754,6 +1772,17 @@
|
|||||||
"path": "nx/documents/view-logs",
|
"path": "nx/documents/view-logs",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"originalFilePath": "generated/cli/view-logs"
|
"originalFilePath": "generated/cli/view-logs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "release",
|
||||||
|
"name": "release",
|
||||||
|
"description": "The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.",
|
||||||
|
"file": "generated/packages/nx/documents/release",
|
||||||
|
"itemList": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"path": "nx/documents/release",
|
||||||
|
"tags": [],
|
||||||
|
"originalFilePath": "generated/cli/release"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"executors": [
|
"executors": [
|
||||||
|
|||||||
32
docs/generated/packages/js/executors/release-publish.json
Normal file
32
docs/generated/packages/js/executors/release-publish.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "release-publish",
|
||||||
|
"implementation": "/packages/js/src/executors/release-publish/release-publish.impl.ts",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"version": 2,
|
||||||
|
"title": "Implementation details of `nx release publish`",
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx run`. Use `nx release publish` instead.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"packageRoot": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The root directory of the directory (containing a manifest file at its root) to publish. Defaults to the project root."
|
||||||
|
},
|
||||||
|
"registry": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The registry to publish the package to."
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The distribution tag to apply to the published package."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx run`. Use `nx release publish` instead.",
|
||||||
|
"hidden": true,
|
||||||
|
"aliases": [],
|
||||||
|
"path": "/packages/js/src/executors/release-publish/schema.json",
|
||||||
|
"type": "executor"
|
||||||
|
}
|
||||||
54
docs/generated/packages/js/generators/release-version.json
Normal file
54
docs/generated/packages/js/generators/release-version.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "release-version",
|
||||||
|
"factory": "./src/generators/release-version/release-version#releaseVersionGenerator",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"$id": "NxJSReleaseVersionGenerator",
|
||||||
|
"cli": "nx",
|
||||||
|
"title": "Implementation details of `nx release version`",
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx generate`. Use `nx release version` instead.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"projects": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The ProjectGraphProjectNodes being versioned in the current execution.",
|
||||||
|
"items": { "type": "object" }
|
||||||
|
},
|
||||||
|
"projectGraph": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "ProjectGraph instance"
|
||||||
|
},
|
||||||
|
"specifier": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Exact version or semver keyword to apply to the selected release group. NOTE: This should be set on the release group level, not the project level."
|
||||||
|
},
|
||||||
|
"preid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The optional prerelease identifier to apply to the version, in the case that specifier has been set to prerelease."
|
||||||
|
},
|
||||||
|
"packageRoot": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The root directory of the directory (containing a manifest file at its root) to publish. Defaults to the project root"
|
||||||
|
},
|
||||||
|
"currentVersionResolver": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "disk",
|
||||||
|
"description": "Which approach to use to determine the current version of the project.",
|
||||||
|
"enum": ["registry", "disk"]
|
||||||
|
},
|
||||||
|
"currentVersionResolverMetadata": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Additional metadata to pass to the current version resolver.",
|
||||||
|
"default": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["projects", "projectGraph", "specifier"],
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx generate`. Use `nx release version` instead.",
|
||||||
|
"hidden": true,
|
||||||
|
"implementation": "/packages/js/src/generators/release-version/release-version#releaseVersionGenerator.ts",
|
||||||
|
"aliases": [],
|
||||||
|
"path": "/packages/js/src/generators/release-version/schema.json",
|
||||||
|
"type": "generator"
|
||||||
|
}
|
||||||
@ -115,7 +115,7 @@ Type: `boolean`
|
|||||||
|
|
||||||
Show help
|
Show help
|
||||||
|
|
||||||
### nx-bail
|
### nxBail
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ Default: `false`
|
|||||||
|
|
||||||
Stop command execution after the first failed task
|
Stop command execution after the first failed task
|
||||||
|
|
||||||
### nx-ignore-cycles
|
### nxIgnoreCycles
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -151,7 +151,7 @@ Type: `string`
|
|||||||
|
|
||||||
This is the name of the tasks runner configured in nx.json
|
This is the name of the tasks runner configured in nx.json
|
||||||
|
|
||||||
### skip-nx-cache
|
### skipNxCache
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ Type: `string`
|
|||||||
|
|
||||||
Show the task graph of the command. Pass a file path to save the graph data instead of viewing it in the browser.
|
Show the task graph of the command. Pass a file path to save the graph data instead of viewing it in the browser.
|
||||||
|
|
||||||
### nx-bail
|
### nxBail
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ Default: `false`
|
|||||||
|
|
||||||
Stop command execution after the first failed task
|
Stop command execution after the first failed task
|
||||||
|
|
||||||
### nx-ignore-cycles
|
### nxIgnoreCycles
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ Type: `string`
|
|||||||
|
|
||||||
This is the name of the tasks runner configured in nx.json
|
This is the name of the tasks runner configured in nx.json
|
||||||
|
|
||||||
### skip-nx-cache
|
### skipNxCache
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
|
|||||||
250
docs/generated/packages/nx/documents/release.md
Normal file
250
docs/generated/packages/nx/documents/release.md
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
---
|
||||||
|
title: 'release - CLI command'
|
||||||
|
description: '**ALPHA**: Orchestrate versioning and publishing of applications and libraries'
|
||||||
|
---
|
||||||
|
|
||||||
|
# release
|
||||||
|
|
||||||
|
**ALPHA**: Orchestrate versioning and publishing of applications and libraries
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nx release
|
||||||
|
```
|
||||||
|
|
||||||
|
Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### dryRun
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Preview the changes without updating files/creating releases
|
||||||
|
|
||||||
|
### groups
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
One or more release groups to target with the current command.
|
||||||
|
|
||||||
|
### help
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show help
|
||||||
|
|
||||||
|
### projects
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Projects to run. (comma/space delimited project names and/or patterns)
|
||||||
|
|
||||||
|
### verbose
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Prints additional information about the commands (e.g., stack traces)
|
||||||
|
|
||||||
|
### version
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show version number
|
||||||
|
|
||||||
|
## Subcommands
|
||||||
|
|
||||||
|
### version
|
||||||
|
|
||||||
|
Create a version and release for one or more applications and libraries
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nx release version [specifier]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
##### help
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show help
|
||||||
|
|
||||||
|
##### preid
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The optional prerelease identifier to apply to the version, in the case that specifier has been set to prerelease.
|
||||||
|
|
||||||
|
##### specifier
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Exact version or semver keyword to apply to the selected release group.
|
||||||
|
|
||||||
|
##### version
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show version number
|
||||||
|
|
||||||
|
### changelog
|
||||||
|
|
||||||
|
Generate a changelog for one or more projects, and optionally push to Github
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nx release changelog [version]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
##### from
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The git reference to use as the start of the changelog. If not set it will attempt to resolve the latest tag and use that
|
||||||
|
|
||||||
|
##### gitRemote
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `origin`
|
||||||
|
|
||||||
|
Alternate git remote in the form {user}/{repo} on which to create the Github release (useful for testing)
|
||||||
|
|
||||||
|
##### help
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show help
|
||||||
|
|
||||||
|
##### interactive
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
##### tagVersionPrefix
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `v`
|
||||||
|
|
||||||
|
Prefix to apply to the version when creating the Github release tag
|
||||||
|
|
||||||
|
##### to
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Default: `HEAD`
|
||||||
|
|
||||||
|
The git reference to use as the end of the changelog
|
||||||
|
|
||||||
|
##### version
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The version to create a Github release and changelog for
|
||||||
|
|
||||||
|
### publish
|
||||||
|
|
||||||
|
Publish a versioned project to a registry
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nx release publish
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
##### all
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
[deprecated] `run-many` runs all targets on all projects in the workspace if no projects are provided. This option is no longer required.
|
||||||
|
|
||||||
|
##### exclude
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Exclude certain projects from being processed
|
||||||
|
|
||||||
|
##### graph
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Show the task graph of the command. Pass a file path to save the graph data instead of viewing it in the browser.
|
||||||
|
|
||||||
|
##### help
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show help
|
||||||
|
|
||||||
|
##### nxBail
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Stop command execution after the first failed task
|
||||||
|
|
||||||
|
##### nxIgnoreCycles
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Ignore cycles in the task graph
|
||||||
|
|
||||||
|
##### parallel
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Max number of parallel processes [default is 3]
|
||||||
|
|
||||||
|
##### projects
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
Projects to run. (comma/space delimited project names and/or patterns)
|
||||||
|
|
||||||
|
##### registry
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The registry to publish to
|
||||||
|
|
||||||
|
##### runner
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
This is the name of the tasks runner configured in nx.json
|
||||||
|
|
||||||
|
##### skipNxCache
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Rerun the tasks even when the results are available in the cache
|
||||||
|
|
||||||
|
##### tag
|
||||||
|
|
||||||
|
Type: `string`
|
||||||
|
|
||||||
|
The distribution tag to apply to the published package
|
||||||
|
|
||||||
|
##### verbose
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Prints additional information about the commands (e.g., stack traces)
|
||||||
|
|
||||||
|
##### version
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Show version number
|
||||||
@ -105,7 +105,7 @@ Type: `boolean`
|
|||||||
|
|
||||||
Show help
|
Show help
|
||||||
|
|
||||||
### nx-bail
|
### nxBail
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ Default: `false`
|
|||||||
|
|
||||||
Stop command execution after the first failed task
|
Stop command execution after the first failed task
|
||||||
|
|
||||||
### nx-ignore-cycles
|
### nxIgnoreCycles
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ Type: `string`
|
|||||||
|
|
||||||
This is the name of the tasks runner configured in nx.json
|
This is the name of the tasks runner configured in nx.json
|
||||||
|
|
||||||
### skip-nx-cache
|
### skipNxCache
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|
||||||
|
|||||||
@ -1985,6 +1985,11 @@
|
|||||||
"name": "view-logs",
|
"name": "view-logs",
|
||||||
"id": "view-logs",
|
"id": "view-logs",
|
||||||
"file": "generated/cli/view-logs"
|
"file": "generated/cli/view-logs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"id": "release",
|
||||||
|
"file": "generated/cli/release"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -434,11 +434,13 @@
|
|||||||
- [tsc](/packages/js/executors/tsc)
|
- [tsc](/packages/js/executors/tsc)
|
||||||
- [swc](/packages/js/executors/swc)
|
- [swc](/packages/js/executors/swc)
|
||||||
- [node](/packages/js/executors/node)
|
- [node](/packages/js/executors/node)
|
||||||
|
- [release-publish](/packages/js/executors/release-publish)
|
||||||
- [verdaccio](/packages/js/executors/verdaccio)
|
- [verdaccio](/packages/js/executors/verdaccio)
|
||||||
- [generators](/packages/js/generators)
|
- [generators](/packages/js/generators)
|
||||||
- [library](/packages/js/generators/library)
|
- [library](/packages/js/generators/library)
|
||||||
- [init](/packages/js/generators/init)
|
- [init](/packages/js/generators/init)
|
||||||
- [convert-to-swc](/packages/js/generators/convert-to-swc)
|
- [convert-to-swc](/packages/js/generators/convert-to-swc)
|
||||||
|
- [release-version](/packages/js/generators/release-version)
|
||||||
- [setup-verdaccio](/packages/js/generators/setup-verdaccio)
|
- [setup-verdaccio](/packages/js/generators/setup-verdaccio)
|
||||||
- [setup-build](/packages/js/generators/setup-build)
|
- [setup-build](/packages/js/generators/setup-build)
|
||||||
- [linter](/packages/linter)
|
- [linter](/packages/linter)
|
||||||
@ -522,6 +524,7 @@
|
|||||||
- [watch](/packages/nx/documents/watch)
|
- [watch](/packages/nx/documents/watch)
|
||||||
- [show](/packages/nx/documents/show)
|
- [show](/packages/nx/documents/show)
|
||||||
- [view-logs](/packages/nx/documents/view-logs)
|
- [view-logs](/packages/nx/documents/view-logs)
|
||||||
|
- [release](/packages/nx/documents/release)
|
||||||
- [executors](/packages/nx/executors)
|
- [executors](/packages/nx/executors)
|
||||||
- [noop](/packages/nx/executors/noop)
|
- [noop](/packages/nx/executors/noop)
|
||||||
- [run-commands](/packages/nx/executors/run-commands)
|
- [run-commands](/packages/nx/executors/run-commands)
|
||||||
|
|||||||
13
e2e/release/jest.config.ts
Normal file
13
e2e/release/jest.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
export default {
|
||||||
|
transform: {
|
||||||
|
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
|
||||||
|
maxWorkers: 1,
|
||||||
|
globals: {},
|
||||||
|
globalSetup: '../utils/global-setup.ts',
|
||||||
|
globalTeardown: '../utils/global-teardown.ts',
|
||||||
|
displayName: 'e2e-release',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
};
|
||||||
10
e2e/release/project.json
Normal file
10
e2e/release/project.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "e2e-release",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "e2e/release",
|
||||||
|
"projectType": "application",
|
||||||
|
"targets": {
|
||||||
|
"e2e": {}
|
||||||
|
},
|
||||||
|
"implicitDependencies": ["nx", "js"]
|
||||||
|
}
|
||||||
393
e2e/release/src/release.test.ts
Normal file
393
e2e/release/src/release.test.ts
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
import { NxJsonConfiguration } from '@nx/devkit';
|
||||||
|
import {
|
||||||
|
cleanupProject,
|
||||||
|
killProcessAndPorts,
|
||||||
|
newProject,
|
||||||
|
runCLI,
|
||||||
|
runCommandUntil,
|
||||||
|
uniq,
|
||||||
|
updateJson,
|
||||||
|
} from '@nx/e2e/utils';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
expect.addSnapshotSerializer({
|
||||||
|
serialize(str: string) {
|
||||||
|
return (
|
||||||
|
str
|
||||||
|
// Remove all output unique to specific projects to ensure deterministic snapshots
|
||||||
|
.replaceAll(/my-pkg-\d+/g, '{project-name}')
|
||||||
|
.replaceAll(
|
||||||
|
/integrity:\s*.*/g,
|
||||||
|
'integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
|
||||||
|
)
|
||||||
|
.replaceAll(/\b[0-9a-f]{40}\b/g, '{SHASUM}')
|
||||||
|
.replaceAll(/\d*B index\.js/g, 'XXB index.js')
|
||||||
|
.replaceAll(/\d*B project\.json/g, 'XXB project.json')
|
||||||
|
.replaceAll(/\d*B package\.json/g, 'XXXB package.json')
|
||||||
|
.replaceAll(/size:\s*\d*\s?B/g, 'size: XXXB')
|
||||||
|
.replaceAll(/\d*\.\d*\s?kB/g, 'XXX.XXX kb')
|
||||||
|
// We trim each line to reduce the chances of snapshot flakiness
|
||||||
|
.split('\n')
|
||||||
|
.map((r) => r.trim())
|
||||||
|
.join('\n')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
test(val: string) {
|
||||||
|
return val != null && typeof val === 'string';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nx release', () => {
|
||||||
|
let pkg1: string;
|
||||||
|
let pkg2: string;
|
||||||
|
let pkg3: string;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
newProject({
|
||||||
|
unsetProjectNameAndRootFormat: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
pkg1 = uniq('my-pkg-1');
|
||||||
|
runCLI(`generate @nx/workspace:npm-package ${pkg1}`);
|
||||||
|
|
||||||
|
pkg2 = uniq('my-pkg-2');
|
||||||
|
runCLI(`generate @nx/workspace:npm-package ${pkg2}`);
|
||||||
|
|
||||||
|
pkg3 = uniq('my-pkg-3');
|
||||||
|
runCLI(`generate @nx/workspace:npm-package ${pkg3}`);
|
||||||
|
|
||||||
|
// Update pkg2 to depend on pkg1
|
||||||
|
updateJson(`${pkg2}/package.json`, (json) => {
|
||||||
|
json.dependencies ??= {};
|
||||||
|
json.dependencies[`@proj/${pkg1}`] = '0.0.0';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterAll(() => cleanupProject());
|
||||||
|
|
||||||
|
it('should version and publish multiple related npm packages with zero config', async () => {
|
||||||
|
const versionOutput = runCLI(`release version 999.9.9`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We can't just assert on the whole version output as a snapshot because the order of the projects
|
||||||
|
* is non-deterministic, and not every project has the same number of log lines (because of the
|
||||||
|
* dependency relationship)
|
||||||
|
*/
|
||||||
|
expect(
|
||||||
|
versionOutput.match(/Running release version for project: my-pkg-\d*/g)
|
||||||
|
.length
|
||||||
|
).toEqual(3);
|
||||||
|
expect(
|
||||||
|
versionOutput.match(
|
||||||
|
/Reading data for package "@proj\/my-pkg-\d*" from my-pkg-\d*\/package.json/g
|
||||||
|
).length
|
||||||
|
).toEqual(3);
|
||||||
|
expect(
|
||||||
|
versionOutput.match(
|
||||||
|
/Resolved the current version as 0.0.0 from my-pkg-\d*\/package.json/g
|
||||||
|
).length
|
||||||
|
).toEqual(3);
|
||||||
|
expect(
|
||||||
|
versionOutput.match(
|
||||||
|
/New version 999.9.9 written to my-pkg-\d*\/package.json/g
|
||||||
|
).length
|
||||||
|
).toEqual(3);
|
||||||
|
|
||||||
|
// Only one dependency relationship exists, so this log should only match once
|
||||||
|
expect(
|
||||||
|
versionOutput.match(
|
||||||
|
/Applying new version 999.9.9 to 1 package which depends on my-pkg-\d*/g
|
||||||
|
).length
|
||||||
|
).toEqual(1);
|
||||||
|
|
||||||
|
// This is the verdaccio instance that the e2e tests themselves are working from
|
||||||
|
const e2eRegistryUrl = execSync('npm config get registry')
|
||||||
|
.toString()
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Thanks to the custom serializer above, the publish output should be deterministic
|
||||||
|
const publishOutput = runCLI(`release publish`);
|
||||||
|
expect(publishOutput).toMatchInlineSnapshot(`
|
||||||
|
|
||||||
|
> NX Running target nx-release-publish for 3 projects:
|
||||||
|
|
||||||
|
- {project-name}
|
||||||
|
- {project-name}
|
||||||
|
- {project-name}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> nx run {project-name}:nx-release-publish
|
||||||
|
|
||||||
|
|
||||||
|
📦 @proj/{project-name}@999.9.9
|
||||||
|
=== Tarball Contents ===
|
||||||
|
|
||||||
|
XXB index.js
|
||||||
|
XXXB package.json
|
||||||
|
XXB project.json
|
||||||
|
=== Tarball Details ===
|
||||||
|
name: @proj/{project-name}
|
||||||
|
version: 999.9.9
|
||||||
|
filename: proj-{project-name}-999.9.9.tgz
|
||||||
|
package size: XXXB
|
||||||
|
unpacked size: XXXB
|
||||||
|
shasum: {SHASUM}
|
||||||
|
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
total files: 3
|
||||||
|
|
||||||
|
Published to ${e2eRegistryUrl} with tag "latest"
|
||||||
|
|
||||||
|
> nx run {project-name}:nx-release-publish
|
||||||
|
|
||||||
|
|
||||||
|
📦 @proj/{project-name}@999.9.9
|
||||||
|
=== Tarball Contents ===
|
||||||
|
|
||||||
|
XXB index.js
|
||||||
|
XXXB package.json
|
||||||
|
XXB project.json
|
||||||
|
=== Tarball Details ===
|
||||||
|
name: @proj/{project-name}
|
||||||
|
version: 999.9.9
|
||||||
|
filename: proj-{project-name}-999.9.9.tgz
|
||||||
|
package size: XXXB
|
||||||
|
unpacked size: XXXB
|
||||||
|
shasum: {SHASUM}
|
||||||
|
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
total files: 3
|
||||||
|
|
||||||
|
Published to ${e2eRegistryUrl} with tag "latest"
|
||||||
|
|
||||||
|
> nx run {project-name}:nx-release-publish
|
||||||
|
|
||||||
|
|
||||||
|
📦 @proj/{project-name}@999.9.9
|
||||||
|
=== Tarball Contents ===
|
||||||
|
|
||||||
|
XXB index.js
|
||||||
|
XXXB package.json
|
||||||
|
XXB project.json
|
||||||
|
=== Tarball Details ===
|
||||||
|
name: @proj/{project-name}
|
||||||
|
version: 999.9.9
|
||||||
|
filename: proj-{project-name}-999.9.9.tgz
|
||||||
|
package size: XXXB
|
||||||
|
unpacked size: XXXB
|
||||||
|
shasum: {SHASUM}
|
||||||
|
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
total files: 3
|
||||||
|
|
||||||
|
Published to ${e2eRegistryUrl} with tag "latest"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> NX Successfully ran target nx-release-publish for 3 projects
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
execSync(`npm view @proj/${pkg1} version`).toString().trim()
|
||||||
|
).toEqual('999.9.9');
|
||||||
|
expect(
|
||||||
|
execSync(`npm view @proj/${pkg2} version`).toString().trim()
|
||||||
|
).toEqual('999.9.9');
|
||||||
|
expect(
|
||||||
|
execSync(`npm view @proj/${pkg3} version`).toString().trim()
|
||||||
|
).toEqual('999.9.9');
|
||||||
|
|
||||||
|
// Add custom nx release config to control version resolution
|
||||||
|
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
|
||||||
|
nxJson.release = {
|
||||||
|
groups: {
|
||||||
|
default: {
|
||||||
|
// @proj/source will be added as a project by the verdaccio setup, but we aren't versioning or publishing it, so we exclude it here
|
||||||
|
projects: ['*', '!@proj/source'],
|
||||||
|
version: {
|
||||||
|
generator: '@nx/js:release-version',
|
||||||
|
generatorOptions: {
|
||||||
|
// Resolve the latest version from the custom registry instance, therefore finding the previously published versions
|
||||||
|
currentVersionResolver: 'registry',
|
||||||
|
currentVersionResolverMetadata: {
|
||||||
|
registry: e2eRegistryUrl,
|
||||||
|
tag: 'latest',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return nxJson;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run additional custom verdaccio instance to publish the packages to
|
||||||
|
runCLI(`generate setup-verdaccio`);
|
||||||
|
|
||||||
|
const verdaccioPort = 7190;
|
||||||
|
const customRegistryUrl = `http://localhost:${verdaccioPort}`;
|
||||||
|
const process = await runCommandUntil(
|
||||||
|
`local-registry @proj/source --port=${verdaccioPort}`,
|
||||||
|
(output) => output.includes(`warn --- http address`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const versionOutput2 = runCLI(`release version premajor --preid next`); // version using semver keyword this time (and custom preid)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
versionOutput2.match(/Running release version for project: my-pkg-\d*/g)
|
||||||
|
.length
|
||||||
|
).toEqual(3);
|
||||||
|
expect(
|
||||||
|
versionOutput2.match(
|
||||||
|
/Reading data for package "@proj\/my-pkg-\d*" from my-pkg-\d*\/package.json/g
|
||||||
|
).length
|
||||||
|
).toEqual(3);
|
||||||
|
|
||||||
|
// It should resolve the current version from the registry once...
|
||||||
|
expect(
|
||||||
|
versionOutput2.match(
|
||||||
|
new RegExp(
|
||||||
|
`Resolved the current version as 999.9.9 for tag "latest" from registry ${e2eRegistryUrl}`,
|
||||||
|
'g'
|
||||||
|
)
|
||||||
|
).length
|
||||||
|
).toEqual(1);
|
||||||
|
// ...and then reuse it twice
|
||||||
|
expect(
|
||||||
|
versionOutput2.match(
|
||||||
|
new RegExp(
|
||||||
|
`Using the current version 999.9.9 already resolved from the registry ${e2eRegistryUrl}`,
|
||||||
|
'g'
|
||||||
|
)
|
||||||
|
).length
|
||||||
|
).toEqual(2);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
versionOutput2.match(
|
||||||
|
/New version 1000.0.0-next.0 written to my-pkg-\d*\/package.json/g
|
||||||
|
).length
|
||||||
|
).toEqual(3);
|
||||||
|
|
||||||
|
// Only one dependency relationship exists, so this log should only match once
|
||||||
|
expect(
|
||||||
|
versionOutput2.match(
|
||||||
|
/Applying new version 1000.0.0-next.0 to 1 package which depends on my-pkg-\d*/g
|
||||||
|
).length
|
||||||
|
).toEqual(1);
|
||||||
|
|
||||||
|
// publish to custom registry (not e2e registry), and a custom dist tag of "next"
|
||||||
|
const publishOutput2 = runCLI(
|
||||||
|
`release publish --registry=${customRegistryUrl} --tag=next`
|
||||||
|
);
|
||||||
|
expect(publishOutput2).toMatchInlineSnapshot(`
|
||||||
|
|
||||||
|
> NX Running target nx-release-publish for 3 projects:
|
||||||
|
|
||||||
|
- {project-name}
|
||||||
|
- {project-name}
|
||||||
|
- {project-name}
|
||||||
|
|
||||||
|
With additional flags:
|
||||||
|
--registry=${customRegistryUrl}
|
||||||
|
--tag=next
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> nx run {project-name}:nx-release-publish
|
||||||
|
|
||||||
|
|
||||||
|
📦 @proj/{project-name}@1000.0.0-next.0
|
||||||
|
=== Tarball Contents ===
|
||||||
|
|
||||||
|
XXB index.js
|
||||||
|
XXXB package.json
|
||||||
|
XXB project.json
|
||||||
|
=== Tarball Details ===
|
||||||
|
name: @proj/{project-name}
|
||||||
|
version: 1000.0.0-next.0
|
||||||
|
filename: proj-{project-name}-1000.0.0-next.0.tgz
|
||||||
|
package size: XXXB
|
||||||
|
unpacked size: XXXB
|
||||||
|
shasum: {SHASUM}
|
||||||
|
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
total files: 3
|
||||||
|
|
||||||
|
Published to ${customRegistryUrl} with tag "next"
|
||||||
|
|
||||||
|
> nx run {project-name}:nx-release-publish
|
||||||
|
|
||||||
|
|
||||||
|
📦 @proj/{project-name}@1000.0.0-next.0
|
||||||
|
=== Tarball Contents ===
|
||||||
|
|
||||||
|
XXB index.js
|
||||||
|
XXXB package.json
|
||||||
|
XXB project.json
|
||||||
|
=== Tarball Details ===
|
||||||
|
name: @proj/{project-name}
|
||||||
|
version: 1000.0.0-next.0
|
||||||
|
filename: proj-{project-name}-1000.0.0-next.0.tgz
|
||||||
|
package size: XXXB
|
||||||
|
unpacked size: XXXB
|
||||||
|
shasum: {SHASUM}
|
||||||
|
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
total files: 3
|
||||||
|
|
||||||
|
Published to ${customRegistryUrl} with tag "next"
|
||||||
|
|
||||||
|
> nx run {project-name}:nx-release-publish
|
||||||
|
|
||||||
|
|
||||||
|
📦 @proj/{project-name}@1000.0.0-next.0
|
||||||
|
=== Tarball Contents ===
|
||||||
|
|
||||||
|
XXB index.js
|
||||||
|
XXXB package.json
|
||||||
|
XXB project.json
|
||||||
|
=== Tarball Details ===
|
||||||
|
name: @proj/{project-name}
|
||||||
|
version: 1000.0.0-next.0
|
||||||
|
filename: proj-{project-name}-1000.0.0-next.0.tgz
|
||||||
|
package size: XXXB
|
||||||
|
unpacked size: XXXB
|
||||||
|
shasum: {SHASUM}
|
||||||
|
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
total files: 3
|
||||||
|
|
||||||
|
Published to ${customRegistryUrl} with tag "next"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> NX Successfully ran target nx-release-publish for 3 projects
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
execSync(
|
||||||
|
`npm view @proj/${pkg1}@next version --registry=${customRegistryUrl}`
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
).toEqual('1000.0.0-next.0');
|
||||||
|
expect(
|
||||||
|
execSync(
|
||||||
|
`npm view @proj/${pkg2}@next version --registry=${customRegistryUrl}`
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
).toEqual('1000.0.0-next.0');
|
||||||
|
expect(
|
||||||
|
execSync(
|
||||||
|
`npm view @proj/${pkg3}@next version --registry=${customRegistryUrl}`
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
).toEqual('1000.0.0-next.0');
|
||||||
|
|
||||||
|
// port and process cleanup
|
||||||
|
await killProcessAndPorts(process.pid, verdaccioPort);
|
||||||
|
}, 500000);
|
||||||
|
});
|
||||||
13
e2e/release/tsconfig.json
Normal file
13
e2e/release/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node", "jest"]
|
||||||
|
},
|
||||||
|
"include": [],
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
20
e2e/release/tsconfig.spec.json
Normal file
20
e2e/release/tsconfig.spec.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.test.jsx",
|
||||||
|
"**/*.d.ts",
|
||||||
|
"jest.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -119,10 +119,11 @@
|
|||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/marked": "^2.0.0",
|
"@types/marked": "^2.0.0",
|
||||||
"@types/node": "18.16.9",
|
"@types/node": "18.16.9",
|
||||||
|
"@types/npm-package-arg": "6.1.1",
|
||||||
"@types/prettier": "^2.6.2",
|
"@types/prettier": "^2.6.2",
|
||||||
"@types/react": "18.2.14",
|
"@types/react": "18.2.14",
|
||||||
"@types/react-dom": "18.2.6",
|
"@types/react-dom": "18.2.6",
|
||||||
"@types/semver": "^7.5.0",
|
"@types/semver": "^7.5.2",
|
||||||
"@types/tar-stream": "^2.2.2",
|
"@types/tar-stream": "^2.2.2",
|
||||||
"@types/tmp": "^0.2.0",
|
"@types/tmp": "^0.2.0",
|
||||||
"@types/yargs": "^17.0.10",
|
"@types/yargs": "^17.0.10",
|
||||||
@ -144,6 +145,7 @@
|
|||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"cli-cursor": "3.1.0",
|
"cli-cursor": "3.1.0",
|
||||||
"cli-spinners": "2.6.1",
|
"cli-spinners": "2.6.1",
|
||||||
|
"columnify": "^1.6.0",
|
||||||
"confusing-browser-globals": "^1.0.9",
|
"confusing-browser-globals": "^1.0.9",
|
||||||
"conventional-changelog-cli": "^2.0.23",
|
"conventional-changelog-cli": "^2.0.23",
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
@ -197,6 +199,7 @@
|
|||||||
"jasmine-spec-reporter": "~4.2.1",
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
"jest": "29.4.3",
|
"jest": "29.4.3",
|
||||||
"jest-config": "^29.4.1",
|
"jest-config": "^29.4.1",
|
||||||
|
"jest-diff": "^29.4.1",
|
||||||
"jest-environment-jsdom": "29.4.3",
|
"jest-environment-jsdom": "29.4.3",
|
||||||
"jest-environment-node": "^29.4.1",
|
"jest-environment-node": "^29.4.1",
|
||||||
"jest-resolve": "^29.4.1",
|
"jest-resolve": "^29.4.1",
|
||||||
@ -225,6 +228,7 @@
|
|||||||
"next-sitemap": "^3.1.10",
|
"next-sitemap": "^3.1.10",
|
||||||
"ng-packagr": "~16.2.0",
|
"ng-packagr": "~16.2.0",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
|
"npm-package-arg": "11.0.1",
|
||||||
"nx": "16.9.0-beta.1",
|
"nx": "16.9.0-beta.1",
|
||||||
"nx-cloud": "16.4.0",
|
"nx-cloud": "16.4.0",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.14",
|
||||||
|
|||||||
@ -17,6 +17,12 @@
|
|||||||
"schema": "./src/executors/node/schema.json",
|
"schema": "./src/executors/node/schema.json",
|
||||||
"description": "Execute a Node application."
|
"description": "Execute a Node application."
|
||||||
},
|
},
|
||||||
|
"release-publish": {
|
||||||
|
"implementation": "./src/executors/release-publish/release-publish.impl",
|
||||||
|
"schema": "./src/executors/release-publish/schema.json",
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx run`. Use `nx release publish` instead.",
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
"verdaccio": {
|
"verdaccio": {
|
||||||
"implementation": "./src/executors/verdaccio/verdaccio.impl",
|
"implementation": "./src/executors/verdaccio/verdaccio.impl",
|
||||||
"schema": "./src/executors/verdaccio/schema.json",
|
"schema": "./src/executors/verdaccio/schema.json",
|
||||||
|
|||||||
@ -59,6 +59,12 @@
|
|||||||
"x-type": "library",
|
"x-type": "library",
|
||||||
"description": "Convert a TypeScript library to compile with SWC."
|
"description": "Convert a TypeScript library to compile with SWC."
|
||||||
},
|
},
|
||||||
|
"release-version": {
|
||||||
|
"factory": "./src/generators/release-version/release-version#releaseVersionGenerator",
|
||||||
|
"schema": "./src/generators/release-version/schema.json",
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx generate`. Use `nx release version` instead.",
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
"setup-verdaccio": {
|
"setup-verdaccio": {
|
||||||
"factory": "./src/generators/setup-verdaccio/generator#setupVerdaccio",
|
"factory": "./src/generators/setup-verdaccio/generator#setupVerdaccio",
|
||||||
"schema": "./src/generators/setup-verdaccio/schema.json",
|
"schema": "./src/generators/setup-verdaccio/schema.json",
|
||||||
|
|||||||
@ -44,14 +44,18 @@
|
|||||||
"babel-plugin-macros": "^2.8.0",
|
"babel-plugin-macros": "^2.8.0",
|
||||||
"babel-plugin-transform-typescript-metadata": "^0.3.1",
|
"babel-plugin-transform-typescript-metadata": "^0.3.1",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
|
"columnify": "^1.6.0",
|
||||||
"detect-port": "^1.5.1",
|
"detect-port": "^1.5.1",
|
||||||
"fast-glob": "3.2.7",
|
"fast-glob": "3.2.7",
|
||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
|
"npm-package-arg": "11.0.1",
|
||||||
|
"npm-run-path": "^4.0.1",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"tsconfig-paths": "^4.1.2",
|
"tsconfig-paths": "^4.1.2",
|
||||||
"ignore": "^5.0.4",
|
"ignore": "^5.0.4",
|
||||||
"js-tokens": "^4.0.0",
|
"js-tokens": "^4.0.0",
|
||||||
"minimatch": "3.0.5",
|
"minimatch": "3.0.5",
|
||||||
|
"ora": "5.3.0",
|
||||||
"semver": "7.5.3",
|
"semver": "7.5.3",
|
||||||
"source-map-support": "0.5.19",
|
"source-map-support": "0.5.19",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
|
|||||||
30
packages/js/src/executors/release-publish/format-bytes.ts
Normal file
30
packages/js/src/executors/release-publish/format-bytes.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Taken from https://github.com/npm/cli/blob/c736b622b8504b07f5a19f631ade42dd40063269/lib/utils/format-bytes.js
|
||||||
|
|
||||||
|
// Convert bytes to printable output, for file reporting in tarballs
|
||||||
|
// Only supports up to GB because that's way larger than anything the registry
|
||||||
|
// supports anyways.
|
||||||
|
|
||||||
|
export const formatBytes = (bytes, space = true) => {
|
||||||
|
let spacer = '';
|
||||||
|
if (space) {
|
||||||
|
spacer = ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes < 1000) {
|
||||||
|
// B
|
||||||
|
return `${bytes}${spacer}B`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes < 1000000) {
|
||||||
|
// kB
|
||||||
|
return `${(bytes / 1000).toFixed(1)}${spacer}kB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes < 1000000000) {
|
||||||
|
// MB
|
||||||
|
return `${(bytes / 1000000).toFixed(1)}${spacer}MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GB
|
||||||
|
return `${(bytes / 1000000000).toFixed(1)}${spacer}GB`;
|
||||||
|
};
|
||||||
76
packages/js/src/executors/release-publish/log-tar.ts
Normal file
76
packages/js/src/executors/release-publish/log-tar.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Adapted from https://github.com/npm/cli/blob/c736b622b8504b07f5a19f631ade42dd40063269/lib/utils/tar.js
|
||||||
|
import * as chalk from 'chalk';
|
||||||
|
import * as columnify from 'columnify';
|
||||||
|
import { formatBytes } from './format-bytes';
|
||||||
|
|
||||||
|
export const logTar = (tarball, opts = {}) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const { unicode = true } = opts;
|
||||||
|
console.log('');
|
||||||
|
console.log(
|
||||||
|
`${unicode ? '📦 ' : 'package:'} ${tarball.name}@${tarball.version}`
|
||||||
|
);
|
||||||
|
console.log(chalk.magenta('=== Tarball Contents ==='));
|
||||||
|
if (tarball.files.length) {
|
||||||
|
console.log('');
|
||||||
|
const columnData = columnify(
|
||||||
|
tarball.files
|
||||||
|
.map((f) => {
|
||||||
|
const bytes = formatBytes(f.size, false);
|
||||||
|
return /^node_modules\//.test(f.path)
|
||||||
|
? null
|
||||||
|
: { path: f.path, size: `${bytes}` };
|
||||||
|
})
|
||||||
|
.filter((f) => f),
|
||||||
|
{
|
||||||
|
include: ['size', 'path'],
|
||||||
|
showHeaders: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
columnData.split('\n').forEach((line) => {
|
||||||
|
console.log(line);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (tarball.bundled.length) {
|
||||||
|
console.log(chalk.magenta('=== Bundled Dependencies ==='));
|
||||||
|
tarball.bundled.forEach((name) => console.log('', name));
|
||||||
|
}
|
||||||
|
console.log(chalk.magenta('=== Tarball Details ==='));
|
||||||
|
console.log(
|
||||||
|
columnify(
|
||||||
|
[
|
||||||
|
{ name: 'name:', value: tarball.name },
|
||||||
|
{ name: 'version:', value: tarball.version },
|
||||||
|
tarball.filename && { name: 'filename:', value: tarball.filename },
|
||||||
|
{ name: 'package size:', value: formatBytes(tarball.size) },
|
||||||
|
{ name: 'unpacked size:', value: formatBytes(tarball.unpackedSize) },
|
||||||
|
{ name: 'shasum:', value: tarball.shasum },
|
||||||
|
{
|
||||||
|
name: 'integrity:',
|
||||||
|
value:
|
||||||
|
tarball.integrity.toString().slice(0, 20) +
|
||||||
|
'[...]' +
|
||||||
|
tarball.integrity.toString().slice(80),
|
||||||
|
},
|
||||||
|
tarball.bundled.length && {
|
||||||
|
name: 'bundled deps:',
|
||||||
|
value: tarball.bundled.length,
|
||||||
|
},
|
||||||
|
tarball.bundled.length && {
|
||||||
|
name: 'bundled files:',
|
||||||
|
value: tarball.entryCount - tarball.files.length,
|
||||||
|
},
|
||||||
|
tarball.bundled.length && {
|
||||||
|
name: 'own files:',
|
||||||
|
value: tarball.files.length,
|
||||||
|
},
|
||||||
|
{ name: 'total files:', value: tarball.entryCount },
|
||||||
|
].filter((x) => x),
|
||||||
|
{
|
||||||
|
include: ['name', 'value'],
|
||||||
|
showHeaders: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log('', '');
|
||||||
|
};
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
import { ExecutorContext, joinPathFragments, readJsonFile } from '@nx/devkit';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { env as appendLocalEnv } from 'npm-run-path';
|
||||||
|
import { logTar } from './log-tar';
|
||||||
|
import { PublishExecutorSchema } from './schema';
|
||||||
|
|
||||||
|
const LARGE_BUFFER = 1024 * 1000000;
|
||||||
|
|
||||||
|
function processEnv(color: boolean) {
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
...appendLocalEnv(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
env.FORCE_COLOR = `${color}`;
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function runExecutor(
|
||||||
|
options: PublishExecutorSchema,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
const projectConfig =
|
||||||
|
context.projectsConfigurations!.projects[context.projectName!]!;
|
||||||
|
|
||||||
|
const packageRoot = joinPathFragments(
|
||||||
|
context.root,
|
||||||
|
options.packageRoot ?? projectConfig.root
|
||||||
|
);
|
||||||
|
|
||||||
|
const npmPublishCommandSegments = [`npm publish --json`];
|
||||||
|
|
||||||
|
if (options.registry) {
|
||||||
|
npmPublishCommandSegments.push(`--registry=${options.registry}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.tag) {
|
||||||
|
npmPublishCommandSegments.push(`--tag=${options.tag}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve values using the `npm config` command so that things like environment variables and `publishConfig`s are accounted for
|
||||||
|
const registry =
|
||||||
|
options.registry ?? execSync(`npm config get registry`).toString().trim();
|
||||||
|
const tag = options.tag ?? execSync(`npm config get tag`).toString().trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const output = execSync(npmPublishCommandSegments.join(' '), {
|
||||||
|
maxBuffer: LARGE_BUFFER,
|
||||||
|
env: processEnv(true),
|
||||||
|
cwd: packageRoot,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const stdoutData = JSON.parse(output.toString());
|
||||||
|
|
||||||
|
// If npm workspaces are in use, the publish output will nest the data under the package name, so we normalize it first
|
||||||
|
const normalizedStdoutData = stdoutData[context.projectName!] ?? stdoutData;
|
||||||
|
logTar(normalizedStdoutData);
|
||||||
|
|
||||||
|
console.log(`Published to ${registry} with tag "${tag}"`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
try {
|
||||||
|
const projectPackageJson = readJsonFile(
|
||||||
|
joinPathFragments(packageRoot, 'package.json')
|
||||||
|
);
|
||||||
|
const name = projectPackageJson.name;
|
||||||
|
const currentVersion = projectPackageJson.version;
|
||||||
|
|
||||||
|
const stdoutData = JSON.parse(err.stdout?.toString() || '{}');
|
||||||
|
if (
|
||||||
|
stdoutData.error?.code === 'EPUBLISHCONFLICT' ||
|
||||||
|
(stdoutData.error?.code === 'E403' &&
|
||||||
|
stdoutData.error?.body?.error?.includes(
|
||||||
|
'You cannot publish over the previously published versions'
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
// If package and project name match, make it terser
|
||||||
|
let packageTxt =
|
||||||
|
name === context.projectName
|
||||||
|
? `package "${name}"`
|
||||||
|
: `package "${name}" from project "${context.projectName}"`;
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
`Skipping ${packageTxt}, as v${currentVersion} has already been published to ${registry} with tag "${tag}"`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('npm publish error:');
|
||||||
|
if (stdoutData.error.summary) {
|
||||||
|
console.error(stdoutData.error.summary);
|
||||||
|
}
|
||||||
|
if (stdoutData.error.detail) {
|
||||||
|
console.error(stdoutData.error.detail);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
// npm v9 onwards seems to guarantee stdout will be well formed JSON when --json is used, so maybe we need to
|
||||||
|
// specify that as minimum supported version? (comes with node 18 and 20 by default)
|
||||||
|
console.error(
|
||||||
|
'Something unexpected went wrong when processing the npm publish output\n',
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/js/src/executors/release-publish/schema.d.ts
vendored
Normal file
5
packages/js/src/executors/release-publish/schema.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface PublishExecutorSchema {
|
||||||
|
packageRoot?: string;
|
||||||
|
registry?: string;
|
||||||
|
tag?: string;
|
||||||
|
}
|
||||||
22
packages/js/src/executors/release-publish/schema.json
Normal file
22
packages/js/src/executors/release-publish/schema.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"version": 2,
|
||||||
|
"title": "Implementation details of `nx release publish`",
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx run`. Use `nx release publish` instead.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"packageRoot": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The root directory of the directory (containing a manifest file at its root) to publish. Defaults to the project root."
|
||||||
|
},
|
||||||
|
"registry": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The registry to publish the package to."
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The distribution tag to apply to the published package."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
import { ProjectGraph, Tree, readJson } from '@nx/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
|
import { releaseVersionGenerator } from './release-version';
|
||||||
|
import { createWorkspaceWithPackageDependencies } from './test-utils/create-workspace-with-package-dependencies';
|
||||||
|
|
||||||
|
// Using the daemon in unit tests would cause jest to never exit
|
||||||
|
process.env.NX_DAEMON = 'false';
|
||||||
|
|
||||||
|
describe('release-version', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
let projectGraph: ProjectGraph;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
|
||||||
|
projectGraph = createWorkspaceWithPackageDependencies(tree, {
|
||||||
|
'my-lib': {
|
||||||
|
projectRoot: 'libs/my-lib',
|
||||||
|
packageName: 'my-lib',
|
||||||
|
version: '0.0.1',
|
||||||
|
packageJsonPath: 'libs/my-lib/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
'project-with-dependency-on-my-pkg': {
|
||||||
|
projectRoot: 'libs/project-with-dependency-on-my-pkg',
|
||||||
|
packageName: 'project-with-dependency-on-my-pkg',
|
||||||
|
version: '0.0.1',
|
||||||
|
packageJsonPath: 'libs/project-with-dependency-on-my-pkg/package.json',
|
||||||
|
localDependencies: [
|
||||||
|
{
|
||||||
|
projectName: 'my-lib',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
version: '0.0.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'project-with-devDependency-on-my-pkg': {
|
||||||
|
projectRoot: 'libs/project-with-devDependency-on-my-pkg',
|
||||||
|
packageName: 'project-with-devDependency-on-my-pkg',
|
||||||
|
version: '0.0.1',
|
||||||
|
packageJsonPath:
|
||||||
|
'libs/project-with-devDependency-on-my-pkg/package.json',
|
||||||
|
localDependencies: [
|
||||||
|
{
|
||||||
|
projectName: 'my-lib',
|
||||||
|
dependencyCollection: 'devDependencies',
|
||||||
|
version: '0.0.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should work with semver keywords and exact semver versions`, async () => {
|
||||||
|
expect(readJson(tree, 'libs/my-lib/package.json').version).toEqual('0.0.1');
|
||||||
|
await releaseVersionGenerator(tree, {
|
||||||
|
projects: Object.values(projectGraph.nodes), // version all projects
|
||||||
|
projectGraph,
|
||||||
|
specifier: 'major',
|
||||||
|
currentVersionResolver: 'disk',
|
||||||
|
});
|
||||||
|
expect(readJson(tree, 'libs/my-lib/package.json').version).toEqual('1.0.0');
|
||||||
|
|
||||||
|
await releaseVersionGenerator(tree, {
|
||||||
|
projects: Object.values(projectGraph.nodes), // version all projects
|
||||||
|
projectGraph,
|
||||||
|
specifier: 'minor',
|
||||||
|
currentVersionResolver: 'disk',
|
||||||
|
});
|
||||||
|
expect(readJson(tree, 'libs/my-lib/package.json').version).toEqual('1.1.0');
|
||||||
|
|
||||||
|
await releaseVersionGenerator(tree, {
|
||||||
|
projects: Object.values(projectGraph.nodes), // version all projects
|
||||||
|
projectGraph,
|
||||||
|
specifier: 'patch',
|
||||||
|
currentVersionResolver: 'disk',
|
||||||
|
});
|
||||||
|
expect(readJson(tree, 'libs/my-lib/package.json').version).toEqual('1.1.1');
|
||||||
|
|
||||||
|
await releaseVersionGenerator(tree, {
|
||||||
|
projects: Object.values(projectGraph.nodes), // version all projects
|
||||||
|
projectGraph,
|
||||||
|
specifier: '1.2.3', // exact version
|
||||||
|
currentVersionResolver: 'disk',
|
||||||
|
});
|
||||||
|
expect(readJson(tree, 'libs/my-lib/package.json').version).toEqual('1.2.3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should apply the updated version to the projects, including updating dependents`, async () => {
|
||||||
|
await releaseVersionGenerator(tree, {
|
||||||
|
projects: Object.values(projectGraph.nodes), // version all projects
|
||||||
|
projectGraph,
|
||||||
|
specifier: 'major',
|
||||||
|
currentVersionResolver: 'disk',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readJson(tree, 'libs/my-lib/package.json')).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"name": "my-lib",
|
||||||
|
"version": "1.0.0",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
readJson(tree, 'libs/project-with-dependency-on-my-pkg/package.json')
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"my-lib": "1.0.0",
|
||||||
|
},
|
||||||
|
"name": "project-with-dependency-on-my-pkg",
|
||||||
|
"version": "1.0.0",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(
|
||||||
|
readJson(tree, 'libs/project-with-devDependency-on-my-pkg/package.json')
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"my-lib": "1.0.0",
|
||||||
|
},
|
||||||
|
"name": "project-with-devDependency-on-my-pkg",
|
||||||
|
"version": "1.0.0",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('not all given projects have package.json files', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tree.delete('libs/my-lib/package.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should error with guidance when not all of the given projects are appropriate for JS versioning`, async () => {
|
||||||
|
await expect(
|
||||||
|
releaseVersionGenerator(tree, {
|
||||||
|
projects: Object.values(projectGraph.nodes), // version all projects
|
||||||
|
projectGraph,
|
||||||
|
specifier: 'major',
|
||||||
|
currentVersionResolver: 'disk',
|
||||||
|
})
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"The project "my-lib" does not have a package.json available at libs/my-lib/package.json.
|
||||||
|
|
||||||
|
To fix this you will either need to add a package.json file at that location, or configure "release" within your nx.json to exclude "my-lib" from the current release group, or amend the packageRoot configuration to point to where the package.json should be."
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
219
packages/js/src/generators/release-version/release-version.ts
Normal file
219
packages/js/src/generators/release-version/release-version.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import {
|
||||||
|
Tree,
|
||||||
|
joinPathFragments,
|
||||||
|
output,
|
||||||
|
readJson,
|
||||||
|
updateJson,
|
||||||
|
workspaceRoot,
|
||||||
|
writeJson,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import * as chalk from 'chalk';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { deriveNewSemverVersion } from 'nx/src/command-line/release/version';
|
||||||
|
import { interpolate } from 'nx/src/tasks-runner/utils';
|
||||||
|
import * as ora from 'ora';
|
||||||
|
import { relative } from 'path';
|
||||||
|
import { ReleaseVersionGeneratorSchema } from './schema';
|
||||||
|
import { resolveLocalPackageDependencies } from './utils/resolve-local-package-dependencies';
|
||||||
|
|
||||||
|
export async function releaseVersionGenerator(
|
||||||
|
tree: Tree,
|
||||||
|
options: ReleaseVersionGeneratorSchema
|
||||||
|
) {
|
||||||
|
const projects = options.projects;
|
||||||
|
|
||||||
|
// Resolve any custom package roots for each project upfront as they will need to be reused during dependency resolution
|
||||||
|
const projectNameToPackageRootMap = new Map<string, string>();
|
||||||
|
for (const project of projects) {
|
||||||
|
projectNameToPackageRootMap.set(
|
||||||
|
project.name,
|
||||||
|
// Default to the project root if no custom packageRoot
|
||||||
|
!options.packageRoot
|
||||||
|
? project.data.root
|
||||||
|
: interpolate(options.packageRoot, {
|
||||||
|
workspaceRoot: '',
|
||||||
|
projectRoot: project.data.root,
|
||||||
|
projectName: project.name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentVersion: string;
|
||||||
|
|
||||||
|
for (const project of projects) {
|
||||||
|
const projectName = project.name;
|
||||||
|
const packageRoot = projectNameToPackageRootMap.get(projectName);
|
||||||
|
const packageJsonPath = joinPathFragments(packageRoot, 'package.json');
|
||||||
|
const workspaceRelativePackageJsonPath = relative(
|
||||||
|
workspaceRoot,
|
||||||
|
packageJsonPath
|
||||||
|
);
|
||||||
|
|
||||||
|
const color = getColor(projectName);
|
||||||
|
const log = (msg: string) => {
|
||||||
|
console.log(color.instance.bold(projectName) + ' ' + msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!tree.exists(packageJsonPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`The project "${projectName}" does not have a package.json available at ${workspaceRelativePackageJsonPath}.
|
||||||
|
|
||||||
|
To fix this you will either need to add a package.json file at that location, or configure "release" within your nx.json to exclude "${projectName}" from the current release group, or amend the packageRoot configuration to point to where the package.json should be.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.logSingleLine(
|
||||||
|
`Running release version for project: ${color.instance.bold(
|
||||||
|
project.name
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectPackageJson = readJson(tree, packageJsonPath);
|
||||||
|
log(
|
||||||
|
`🔍 Reading data for package "${projectPackageJson.name}" from ${workspaceRelativePackageJsonPath}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const { name: packageName, version: currentVersionFromDisk } =
|
||||||
|
projectPackageJson;
|
||||||
|
|
||||||
|
switch (options.currentVersionResolver) {
|
||||||
|
case 'registry': {
|
||||||
|
const metadata = options.currentVersionResolverMetadata;
|
||||||
|
const registry = metadata?.registry ?? 'https://registry.npmjs.org';
|
||||||
|
const tag = metadata?.tag ?? 'latest';
|
||||||
|
|
||||||
|
// If the currentVersionResolver is set to registry, we only want to make the request once for the whole batch of projects
|
||||||
|
if (!currentVersion) {
|
||||||
|
const spinner = ora(
|
||||||
|
`${Array.from(new Array(projectName.length + 3)).join(
|
||||||
|
' '
|
||||||
|
)}Resolving the current version for tag "${tag}" on ${registry}`
|
||||||
|
);
|
||||||
|
spinner.color =
|
||||||
|
color.spinnerColor as typeof colors[number]['spinnerColor'];
|
||||||
|
spinner.start();
|
||||||
|
|
||||||
|
// Must be non-blocking async to allow spinner to render
|
||||||
|
currentVersion = await new Promise<string>((resolve, reject) => {
|
||||||
|
exec(
|
||||||
|
`npm view ${packageName} version --registry=${registry} --tag=${tag}`,
|
||||||
|
(error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
return reject(stderr);
|
||||||
|
}
|
||||||
|
return resolve(stdout.trim());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
spinner.stop();
|
||||||
|
|
||||||
|
log(
|
||||||
|
`📄 Resolved the current version as ${currentVersion} for tag "${tag}" from registry ${registry}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log(
|
||||||
|
`📄 Using the current version ${currentVersion} already resolved from the registry ${registry}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'disk':
|
||||||
|
currentVersion = currentVersionFromDisk;
|
||||||
|
log(
|
||||||
|
`📄 Resolved the current version as ${currentVersion} from ${packageJsonPath}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Invalid value for options.currentVersionResolver: ${options.currentVersionResolver}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve any local package dependencies for this project (before applying the new version)
|
||||||
|
const localPackageDependencies = resolveLocalPackageDependencies(
|
||||||
|
tree,
|
||||||
|
options.projectGraph,
|
||||||
|
projects,
|
||||||
|
projectNameToPackageRootMap
|
||||||
|
);
|
||||||
|
|
||||||
|
const newVersion = deriveNewSemverVersion(
|
||||||
|
currentVersion,
|
||||||
|
options.specifier,
|
||||||
|
options.preid
|
||||||
|
);
|
||||||
|
|
||||||
|
writeJson(tree, packageJsonPath, {
|
||||||
|
...projectPackageJson,
|
||||||
|
version: newVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
log(
|
||||||
|
`✍️ New version ${newVersion} written to ${workspaceRelativePackageJsonPath}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const dependentProjects = Object.values(localPackageDependencies)
|
||||||
|
.filter((localPackageDependencies) => {
|
||||||
|
return localPackageDependencies.some(
|
||||||
|
(localPackageDependency) =>
|
||||||
|
localPackageDependency.target === project.name
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
if (dependentProjects.length > 0) {
|
||||||
|
log(
|
||||||
|
`✍️ Applying new version ${newVersion} to ${
|
||||||
|
dependentProjects.length
|
||||||
|
} ${
|
||||||
|
dependentProjects.length > 1
|
||||||
|
? 'packages which depend'
|
||||||
|
: 'package which depends'
|
||||||
|
} on ${project.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dependentProject of dependentProjects) {
|
||||||
|
updateJson(
|
||||||
|
tree,
|
||||||
|
joinPathFragments(
|
||||||
|
projectNameToPackageRootMap.get(dependentProject.source),
|
||||||
|
'package.json'
|
||||||
|
),
|
||||||
|
(json) => {
|
||||||
|
json[dependentProject.dependencyCollection][packageName] = newVersion;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default releaseVersionGenerator;
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
{ instance: chalk.green, spinnerColor: 'green' },
|
||||||
|
{ instance: chalk.greenBright, spinnerColor: 'green' },
|
||||||
|
{ instance: chalk.red, spinnerColor: 'red' },
|
||||||
|
{ instance: chalk.redBright, spinnerColor: 'red' },
|
||||||
|
{ instance: chalk.cyan, spinnerColor: 'cyan' },
|
||||||
|
{ instance: chalk.cyanBright, spinnerColor: 'cyan' },
|
||||||
|
{ instance: chalk.yellow, spinnerColor: 'yellow' },
|
||||||
|
{ instance: chalk.yellowBright, spinnerColor: 'yellow' },
|
||||||
|
{ instance: chalk.magenta, spinnerColor: 'magenta' },
|
||||||
|
{ instance: chalk.magentaBright, spinnerColor: 'magenta' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function getColor(projectName: string) {
|
||||||
|
let code = 0;
|
||||||
|
for (let i = 0; i < projectName.length; ++i) {
|
||||||
|
code += projectName.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const colorIndex = code % colors.length;
|
||||||
|
|
||||||
|
return colors[colorIndex];
|
||||||
|
}
|
||||||
1
packages/js/src/generators/release-version/schema.d.ts
vendored
Normal file
1
packages/js/src/generators/release-version/schema.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { ReleaseVersionGeneratorSchema } from 'nx/src/command-line/release/version';
|
||||||
45
packages/js/src/generators/release-version/schema.json
Normal file
45
packages/js/src/generators/release-version/schema.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"$id": "NxJSReleaseVersionGenerator",
|
||||||
|
"cli": "nx",
|
||||||
|
"title": "Implementation details of `nx release version`",
|
||||||
|
"description": "DO NOT INVOKE DIRECTLY WITH `nx generate`. Use `nx release version` instead.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"projects": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The ProjectGraphProjectNodes being versioned in the current execution.",
|
||||||
|
"items": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectGraph": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "ProjectGraph instance"
|
||||||
|
},
|
||||||
|
"specifier": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Exact version or semver keyword to apply to the selected release group. NOTE: This should be set on the release group level, not the project level."
|
||||||
|
},
|
||||||
|
"preid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The optional prerelease identifier to apply to the version, in the case that specifier has been set to prerelease."
|
||||||
|
},
|
||||||
|
"packageRoot": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The root directory of the directory (containing a manifest file at its root) to publish. Defaults to the project root"
|
||||||
|
},
|
||||||
|
"currentVersionResolver": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "disk",
|
||||||
|
"description": "Which approach to use to determine the current version of the project.",
|
||||||
|
"enum": ["registry", "disk"]
|
||||||
|
},
|
||||||
|
"currentVersionResolverMetadata": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Additional metadata to pass to the current version resolver.",
|
||||||
|
"default": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["projects", "projectGraph", "specifier"]
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { ProjectGraph, Tree, writeJson } from '@nx/devkit';
|
||||||
|
|
||||||
|
interface ProjectAndPackageData {
|
||||||
|
[projectName: string]: {
|
||||||
|
projectRoot: string;
|
||||||
|
packageName: string;
|
||||||
|
version: string;
|
||||||
|
packageJsonPath: string;
|
||||||
|
localDependencies: {
|
||||||
|
projectName: string;
|
||||||
|
dependencyCollection:
|
||||||
|
| 'dependencies'
|
||||||
|
| 'devDependencies'
|
||||||
|
| 'optionalDependencies';
|
||||||
|
version: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWorkspaceWithPackageDependencies(
|
||||||
|
tree: Tree,
|
||||||
|
projectAndPackageData: ProjectAndPackageData
|
||||||
|
): ProjectGraph {
|
||||||
|
const projectGraph: ProjectGraph = {
|
||||||
|
nodes: {},
|
||||||
|
dependencies: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [projectName, data] of Object.entries(projectAndPackageData)) {
|
||||||
|
const packageJsonContents = {
|
||||||
|
name: data.packageName,
|
||||||
|
version: data.version,
|
||||||
|
};
|
||||||
|
for (const dependency of data.localDependencies) {
|
||||||
|
const dependencyPackageName =
|
||||||
|
projectAndPackageData[dependency.projectName].packageName;
|
||||||
|
packageJsonContents[dependency.dependencyCollection] = {
|
||||||
|
...packageJsonContents[dependency.dependencyCollection],
|
||||||
|
[dependencyPackageName]: dependency.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// add the project and its nx project level dependencies to the projectGraph
|
||||||
|
projectGraph.nodes[projectName] = {
|
||||||
|
name: projectName,
|
||||||
|
type: 'lib',
|
||||||
|
data: {
|
||||||
|
root: data.projectRoot,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
projectGraph.dependencies[projectName] = data.localDependencies.map(
|
||||||
|
(dependency) => ({
|
||||||
|
source: projectName,
|
||||||
|
target: dependency.projectName,
|
||||||
|
type: 'static',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// create the package.json in the tree
|
||||||
|
writeJson(tree, data.packageJsonPath, packageJsonContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectGraph;
|
||||||
|
}
|
||||||
43
packages/js/src/generators/release-version/utils/package.ts
Normal file
43
packages/js/src/generators/release-version/utils/package.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { joinPathFragments } from '@nx/devkit';
|
||||||
|
import { PackageJson } from 'nx/src/utils/package-json';
|
||||||
|
|
||||||
|
export class Package {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
location: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private packageJson: PackageJson,
|
||||||
|
workspaceRoot: string,
|
||||||
|
workspaceRelativeLocation: string
|
||||||
|
) {
|
||||||
|
this.name = packageJson.name;
|
||||||
|
this.version = packageJson.version;
|
||||||
|
this.location = joinPathFragments(workspaceRoot, workspaceRelativeLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalDependency(depName: string): {
|
||||||
|
collection: 'dependencies' | 'devDependencies' | 'optionalDependencies';
|
||||||
|
spec: string;
|
||||||
|
} | null {
|
||||||
|
if (this.packageJson.dependencies?.[depName]) {
|
||||||
|
return {
|
||||||
|
collection: 'dependencies',
|
||||||
|
spec: this.packageJson.dependencies[depName],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (this.packageJson.devDependencies?.[depName]) {
|
||||||
|
return {
|
||||||
|
collection: 'devDependencies',
|
||||||
|
spec: this.packageJson.devDependencies[depName],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (this.packageJson.optionalDependencies?.[depName]) {
|
||||||
|
return {
|
||||||
|
collection: 'optionalDependencies',
|
||||||
|
spec: this.packageJson.optionalDependencies[depName],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,410 @@
|
|||||||
|
import { ProjectGraph, Tree, workspaceRoot } from '@nx/devkit';
|
||||||
|
import { createTree } from '@nx/devkit/testing';
|
||||||
|
import { createWorkspaceWithPackageDependencies } from '../test-utils/create-workspace-with-package-dependencies';
|
||||||
|
import { resolveLocalPackageDependencies } from './resolve-local-package-dependencies';
|
||||||
|
|
||||||
|
expect.addSnapshotSerializer({
|
||||||
|
serialize: (str: string) => {
|
||||||
|
// replace all instances of the workspace root with a placeholder to ensure consistency
|
||||||
|
return JSON.stringify(
|
||||||
|
str.replaceAll(
|
||||||
|
new RegExp(workspaceRoot.replace(/\\/g, '\\\\'), 'g'),
|
||||||
|
'<workspaceRoot>'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
test(val: string) {
|
||||||
|
return (
|
||||||
|
val != null && typeof val === 'string' && val.includes(workspaceRoot)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveLocalPackageDependencies()', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
let projectGraph: ProjectGraph;
|
||||||
|
|
||||||
|
describe('fixed versions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTree();
|
||||||
|
|
||||||
|
projectGraph = createWorkspaceWithPackageDependencies(tree, {
|
||||||
|
projectA: {
|
||||||
|
projectRoot: 'packages/projectA',
|
||||||
|
packageName: 'projectA',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectA/package.json',
|
||||||
|
localDependencies: [
|
||||||
|
{
|
||||||
|
projectName: 'projectB',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectName: 'projectC',
|
||||||
|
dependencyCollection: 'devDependencies',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectName: 'projectD',
|
||||||
|
dependencyCollection: 'optionalDependencies',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
projectB: {
|
||||||
|
projectRoot: 'packages/projectB',
|
||||||
|
packageName: 'projectB',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectB/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
projectC: {
|
||||||
|
projectRoot: 'packages/projectC',
|
||||||
|
packageName: 'projectC',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectC/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
projectD: {
|
||||||
|
projectRoot: 'packages/projectD',
|
||||||
|
packageName: 'projectD',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectD/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve local dependencies based on fixed semver versions', () => {
|
||||||
|
const allProjects = Object.values(projectGraph.nodes);
|
||||||
|
const projectNameToPackageRootMap = new Map<string, string>();
|
||||||
|
for (const project of allProjects) {
|
||||||
|
projectNameToPackageRootMap.set(project.name, project.data.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = resolveLocalPackageDependencies(
|
||||||
|
tree,
|
||||||
|
projectGraph,
|
||||||
|
allProjects,
|
||||||
|
projectNameToPackageRootMap
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"projectA": [
|
||||||
|
{
|
||||||
|
"dependencyCollection": "dependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectB",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dependencyCollection": "devDependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectC",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dependencyCollection": "optionalDependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectD",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`"file:", "link:" and "workspace:" protocols`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTree();
|
||||||
|
|
||||||
|
projectGraph = createWorkspaceWithPackageDependencies(tree, {
|
||||||
|
projectA: {
|
||||||
|
projectRoot: 'packages/projectA',
|
||||||
|
packageName: 'projectA',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectA/package.json',
|
||||||
|
localDependencies: [
|
||||||
|
{
|
||||||
|
projectName: 'projectB',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
version: 'file:../projectB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectName: 'projectC',
|
||||||
|
dependencyCollection: 'devDependencies',
|
||||||
|
version: 'workspace:*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectName: 'projectD',
|
||||||
|
dependencyCollection: 'optionalDependencies',
|
||||||
|
version: 'workspace:../projectD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectName: 'projectE',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
version: 'link:../projectE', // yarn classic equivalent of `file:`
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
projectB: {
|
||||||
|
projectRoot: 'packages/projectB',
|
||||||
|
packageName: 'projectB',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectB/package.json',
|
||||||
|
localDependencies: [
|
||||||
|
{
|
||||||
|
projectName: 'projectC',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
version: 'workspace:1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectName: 'projectD',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
/**
|
||||||
|
* Wrong version is specified, shouldn't be resolved as a local package dependency
|
||||||
|
* (pnpm will likely error on this at install time anyway, so it's unlikely
|
||||||
|
* to occur in a real-world setup)
|
||||||
|
*/
|
||||||
|
version: 'workspace:2.0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
projectC: {
|
||||||
|
projectRoot: 'packages/projectC',
|
||||||
|
packageName: 'projectC',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectC/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
projectD: {
|
||||||
|
projectRoot: 'packages/projectD',
|
||||||
|
packageName: 'projectD',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectD/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
projectE: {
|
||||||
|
projectRoot: 'packages/projectE',
|
||||||
|
packageName: 'projectE',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectE/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve local dependencies based on file, link and workspace protocols', () => {
|
||||||
|
const allProjects = Object.values(projectGraph.nodes);
|
||||||
|
const projectNameToPackageRootMap = new Map<string, string>();
|
||||||
|
for (const project of allProjects) {
|
||||||
|
projectNameToPackageRootMap.set(project.name, project.data.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = resolveLocalPackageDependencies(
|
||||||
|
tree,
|
||||||
|
projectGraph,
|
||||||
|
allProjects,
|
||||||
|
projectNameToPackageRootMap
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"projectA": [
|
||||||
|
{
|
||||||
|
"dependencyCollection": "dependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectB",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dependencyCollection": "devDependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectC",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dependencyCollection": "optionalDependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectD",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dependencyCollection": "dependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectE",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"projectB": [
|
||||||
|
{
|
||||||
|
"dependencyCollection": "dependencies",
|
||||||
|
"source": "projectB",
|
||||||
|
"target": "projectC",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('npm scopes', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTree();
|
||||||
|
|
||||||
|
projectGraph = createWorkspaceWithPackageDependencies(tree, {
|
||||||
|
projectA: {
|
||||||
|
projectRoot: 'packages/projectA',
|
||||||
|
packageName: '@acme/projectA',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectA/package.json',
|
||||||
|
localDependencies: [
|
||||||
|
{
|
||||||
|
projectName: 'projectB',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
projectB: {
|
||||||
|
projectRoot: 'packages/projectB',
|
||||||
|
packageName: '@acme/projectB',
|
||||||
|
version: '1.0.0',
|
||||||
|
packageJsonPath: 'packages/projectB/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve local dependencies which contain npm scopes', () => {
|
||||||
|
const allProjects = Object.values(projectGraph.nodes);
|
||||||
|
const projectNameToPackageRootMap = new Map<string, string>();
|
||||||
|
for (const project of allProjects) {
|
||||||
|
projectNameToPackageRootMap.set(project.name, project.data.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = resolveLocalPackageDependencies(
|
||||||
|
tree,
|
||||||
|
projectGraph,
|
||||||
|
allProjects,
|
||||||
|
projectNameToPackageRootMap
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"projectA": [
|
||||||
|
{
|
||||||
|
"dependencyCollection": "dependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectB",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('custom package roots', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTree();
|
||||||
|
|
||||||
|
projectGraph = createWorkspaceWithPackageDependencies(tree, {
|
||||||
|
projectA: {
|
||||||
|
projectRoot: 'packages/projectA',
|
||||||
|
packageName: '@acme/projectA',
|
||||||
|
version: '1.0.0',
|
||||||
|
// Custom package.json path coming from a build/dist location, not the project root
|
||||||
|
packageJsonPath: 'build/packages/projectA/package.json',
|
||||||
|
localDependencies: [
|
||||||
|
{
|
||||||
|
projectName: 'projectB',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectName: 'projectC',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectName: 'projectD',
|
||||||
|
dependencyCollection: 'dependencies',
|
||||||
|
// relative from projectA's package.json path to projectD's package.json path
|
||||||
|
version: 'file:../../../packages/projectD',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
projectB: {
|
||||||
|
projectRoot: 'packages/projectB',
|
||||||
|
packageName: '@acme/projectB',
|
||||||
|
version: '1.0.0',
|
||||||
|
// Custom package.json path coming from a build/dist location, not the project root
|
||||||
|
packageJsonPath: 'build/packages/projectB/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
projectC: {
|
||||||
|
projectRoot: 'packages/projectC',
|
||||||
|
packageName: '@acme/projectC',
|
||||||
|
version: '1.0.0',
|
||||||
|
// Standard package.json path coming from the project root
|
||||||
|
packageJsonPath: 'packages/projectC/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
projectD: {
|
||||||
|
projectRoot: 'packages/projectD',
|
||||||
|
packageName: 'projectD',
|
||||||
|
version: '1.0.0',
|
||||||
|
// Standard package.json path coming from the project root
|
||||||
|
packageJsonPath: 'packages/projectD/package.json',
|
||||||
|
localDependencies: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve local dependencies using custom package roots', () => {
|
||||||
|
const allProjects = Object.values(projectGraph.nodes);
|
||||||
|
const projectNameToPackageRootMap = new Map<string, string>();
|
||||||
|
projectNameToPackageRootMap.set('projectA', 'build/packages/projectA');
|
||||||
|
projectNameToPackageRootMap.set('projectB', 'build/packages/projectB');
|
||||||
|
projectNameToPackageRootMap.set('projectC', 'packages/projectC');
|
||||||
|
projectNameToPackageRootMap.set('projectD', 'packages/projectD');
|
||||||
|
|
||||||
|
const result = resolveLocalPackageDependencies(
|
||||||
|
tree,
|
||||||
|
projectGraph,
|
||||||
|
allProjects,
|
||||||
|
projectNameToPackageRootMap
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"projectA": [
|
||||||
|
{
|
||||||
|
"dependencyCollection": "dependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectB",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dependencyCollection": "dependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectC",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dependencyCollection": "dependencies",
|
||||||
|
"source": "projectA",
|
||||||
|
"target": "projectD",
|
||||||
|
"type": "static",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
ProjectGraph,
|
||||||
|
ProjectGraphDependency,
|
||||||
|
ProjectGraphProjectNode,
|
||||||
|
Tree,
|
||||||
|
joinPathFragments,
|
||||||
|
readJson,
|
||||||
|
workspaceRoot,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { PackageJson } from 'nx/src/utils/package-json';
|
||||||
|
import { satisfies } from 'semver';
|
||||||
|
import { Package } from './package';
|
||||||
|
import { resolveVersionSpec } from './resolve-version-spec';
|
||||||
|
|
||||||
|
interface LocalPackageDependency extends ProjectGraphDependency {
|
||||||
|
dependencyCollection:
|
||||||
|
| 'dependencies'
|
||||||
|
| 'devDependencies'
|
||||||
|
| 'optionalDependencies';
|
||||||
|
// we don't currently manage peer dependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLocalPackageDependencies(
|
||||||
|
tree: Tree,
|
||||||
|
projectGraph: ProjectGraph,
|
||||||
|
projects: ProjectGraphProjectNode[],
|
||||||
|
projectNameToPackageRootMap: Map<string, string>
|
||||||
|
): Record<string, LocalPackageDependency[]> {
|
||||||
|
const localPackageDependencies: Record<string, LocalPackageDependency[]> = {};
|
||||||
|
const projectNodeToPackageMap = new Map<ProjectGraphProjectNode, Package>();
|
||||||
|
|
||||||
|
// Iterate through the projects being released and resolve any relevant package.json data
|
||||||
|
for (const projectNode of projects) {
|
||||||
|
// Resolve the package.json path for the project, taking into account any custom packageRoot settings
|
||||||
|
const packageRoot = projectNameToPackageRootMap.get(projectNode.name);
|
||||||
|
if (!packageRoot) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const packageJsonPath = joinPathFragments(packageRoot, 'package.json');
|
||||||
|
if (!tree.exists(packageJsonPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const packageJson = readJson(tree, packageJsonPath) as PackageJson;
|
||||||
|
const pkg = new Package(packageJson, workspaceRoot, packageRoot);
|
||||||
|
projectNodeToPackageMap.set(projectNode, pkg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate local npm package dependencies
|
||||||
|
for (const projectDeps of Object.values(projectGraph.dependencies)) {
|
||||||
|
const workspaceDeps = projectDeps.filter(
|
||||||
|
(dep) =>
|
||||||
|
!isExternalNpmDependency(dep.target) &&
|
||||||
|
!isExternalNpmDependency(dep.source)
|
||||||
|
);
|
||||||
|
for (const dep of workspaceDeps) {
|
||||||
|
const source = projectGraph.nodes[dep.source];
|
||||||
|
const target = projectGraph.nodes[dep.target];
|
||||||
|
if (
|
||||||
|
!source ||
|
||||||
|
!projectNodeToPackageMap.has(source) ||
|
||||||
|
!target ||
|
||||||
|
!projectNodeToPackageMap.has(target)
|
||||||
|
) {
|
||||||
|
// only relevant for dependencies between two workspace projects with Package objects
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourcePackage = projectNodeToPackageMap.get(source);
|
||||||
|
const targetPackage = projectNodeToPackageMap.get(target);
|
||||||
|
const sourceNpmDependency = sourcePackage.getLocalDependency(
|
||||||
|
targetPackage.name
|
||||||
|
);
|
||||||
|
if (!sourceNpmDependency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetVersionSpec = resolveVersionSpec(
|
||||||
|
targetPackage.name,
|
||||||
|
targetPackage.version,
|
||||||
|
sourceNpmDependency.spec,
|
||||||
|
sourcePackage.location
|
||||||
|
);
|
||||||
|
const targetMatchesRequirement =
|
||||||
|
// For file: and workspace: protocols the targetVersionSpec could be a path, so we check if it matches the target's location
|
||||||
|
targetVersionSpec === targetPackage.location ||
|
||||||
|
satisfies(targetPackage.version, targetVersionSpec);
|
||||||
|
|
||||||
|
if (targetMatchesRequirement) {
|
||||||
|
// track only local package dependencies that are satisfied by the target's version
|
||||||
|
localPackageDependencies[dep.source] = [
|
||||||
|
...(localPackageDependencies[dep.source] || []),
|
||||||
|
{
|
||||||
|
...dep,
|
||||||
|
dependencyCollection: sourceNpmDependency.collection,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localPackageDependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExternalNpmDependency(dep: string): boolean {
|
||||||
|
return dep.startsWith('npm:');
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { resolveVersionSpec } from './resolve-version-spec';
|
||||||
|
|
||||||
|
describe('resolveVersionSpec()', () => {
|
||||||
|
it('should work for specific name and spec', () => {
|
||||||
|
expect(
|
||||||
|
resolveVersionSpec(
|
||||||
|
'projectA',
|
||||||
|
'1.0.4',
|
||||||
|
'^1.0.0',
|
||||||
|
'/test/packages/packageB'
|
||||||
|
)
|
||||||
|
).toEqual('^1.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for a workspace spec', () => {
|
||||||
|
expect(
|
||||||
|
resolveVersionSpec(
|
||||||
|
'projectA',
|
||||||
|
'1.0.4',
|
||||||
|
'workspace:^1.0.0',
|
||||||
|
'/test/packages/packageB'
|
||||||
|
)
|
||||||
|
).toEqual('^1.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a workspace alias', () => {
|
||||||
|
it('should work for a * workspace alias', () => {
|
||||||
|
expect(
|
||||||
|
resolveVersionSpec(
|
||||||
|
'projectA',
|
||||||
|
'1.0.4',
|
||||||
|
'workspace:*',
|
||||||
|
'/test/packages/packageB'
|
||||||
|
)
|
||||||
|
).toEqual('1.0.4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for a ^ workspace alias', () => {
|
||||||
|
expect(
|
||||||
|
resolveVersionSpec(
|
||||||
|
'projectA',
|
||||||
|
'1.0.4',
|
||||||
|
'workspace:^',
|
||||||
|
'/test/packages/packageB'
|
||||||
|
)
|
||||||
|
).toEqual('^1.0.4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for a ~ workspace alias', () => {
|
||||||
|
expect(
|
||||||
|
resolveVersionSpec(
|
||||||
|
'projectA',
|
||||||
|
'1.0.4',
|
||||||
|
'workspace:~',
|
||||||
|
'/test/packages/packageB'
|
||||||
|
)
|
||||||
|
).toEqual('~1.0.4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should for a file reference', async () => {
|
||||||
|
expect(
|
||||||
|
resolveVersionSpec(
|
||||||
|
'projectA',
|
||||||
|
'1.0.0',
|
||||||
|
'file:../projectB',
|
||||||
|
'/packages/projectB'
|
||||||
|
)
|
||||||
|
).toEqual(expect.stringContaining(join('/packages/projectB')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for a yarn classic style link reference', async () => {
|
||||||
|
expect(
|
||||||
|
resolveVersionSpec(
|
||||||
|
'projectA',
|
||||||
|
'1.0.0',
|
||||||
|
'link:../projectB',
|
||||||
|
'/packages/fuck'
|
||||||
|
)
|
||||||
|
).toEqual(expect.stringContaining(join('/packages/projectB')));
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import * as npa from 'npm-package-arg';
|
||||||
|
|
||||||
|
export function resolveVersionSpec(
|
||||||
|
name: string,
|
||||||
|
version: string,
|
||||||
|
spec: string,
|
||||||
|
location?: string
|
||||||
|
): string {
|
||||||
|
// yarn classic uses link instead of file, normalize to match what npm expects
|
||||||
|
spec = spec.replace(/^link:/, 'file:');
|
||||||
|
|
||||||
|
// Support workspace: protocol for pnpm and yarn 2+ (https://pnpm.io/workspaces#workspace-protocol-workspace)
|
||||||
|
const isWorkspaceSpec = /^workspace:/.test(spec);
|
||||||
|
if (isWorkspaceSpec) {
|
||||||
|
spec = spec.replace(/^workspace:/, '');
|
||||||
|
// replace aliases (https://pnpm.io/workspaces#referencing-workspace-packages-through-aliases)
|
||||||
|
if (spec === '*' || spec === '^' || spec === '~') {
|
||||||
|
if (version) {
|
||||||
|
const prefix = spec === '*' ? '' : spec;
|
||||||
|
spec = `${prefix}${version}`;
|
||||||
|
} else {
|
||||||
|
spec = '*';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const npaResult = npa.resolve(name, spec, location);
|
||||||
|
|
||||||
|
return npaResult.fetchSpec;
|
||||||
|
}
|
||||||
@ -49,6 +49,7 @@
|
|||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"glob": "7.1.4",
|
"glob": "7.1.4",
|
||||||
"ignore": "^5.0.4",
|
"ignore": "^5.0.4",
|
||||||
|
"jest-diff": "^29.4.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsonc-parser": "3.2.0",
|
"jsonc-parser": "3.2.0",
|
||||||
"lines-and-columns": "~2.0.3",
|
"lines-and-columns": "~2.0.3",
|
||||||
|
|||||||
@ -52,6 +52,13 @@ describe('nx package.json workspaces plugin', () => {
|
|||||||
"script": "echo",
|
"script": "echo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"nx-release-publish": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^nx-release-publish",
|
||||||
|
],
|
||||||
|
"executor": "@nx/js:release-publish",
|
||||||
|
"options": {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -67,6 +74,13 @@ describe('nx package.json workspaces plugin', () => {
|
|||||||
"root": "packages/lib-a",
|
"root": "packages/lib-a",
|
||||||
"sourceRoot": "packages/lib-a",
|
"sourceRoot": "packages/lib-a",
|
||||||
"targets": {
|
"targets": {
|
||||||
|
"nx-release-publish": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^nx-release-publish",
|
||||||
|
],
|
||||||
|
"executor": "@nx/js:release-publish",
|
||||||
|
"options": {},
|
||||||
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"executor": "nx:run-script",
|
"executor": "nx:run-script",
|
||||||
"options": {
|
"options": {
|
||||||
@ -104,6 +118,13 @@ describe('nx package.json workspaces plugin', () => {
|
|||||||
"{projectRoot}/dist",
|
"{projectRoot}/dist",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"nx-release-publish": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^nx-release-publish",
|
||||||
|
],
|
||||||
|
"executor": "@nx/js:release-publish",
|
||||||
|
"options": {},
|
||||||
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"executor": "nx:run-script",
|
"executor": "nx:run-script",
|
||||||
"options": {
|
"options": {
|
||||||
|
|||||||
@ -104,7 +104,7 @@ export async function affected(
|
|||||||
projectNames
|
projectNames
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await runCommand(
|
const status = await runCommand(
|
||||||
projectsWithTarget,
|
projectsWithTarget,
|
||||||
projectGraph,
|
projectGraph,
|
||||||
{ nxJson },
|
{ nxJson },
|
||||||
@ -114,6 +114,9 @@ export async function affected(
|
|||||||
extraTargetDependencies,
|
extraTargetDependencies,
|
||||||
{ excludeTaskDependencies: false, loadDotEnvFiles: true }
|
{ excludeTaskDependencies: false, loadDotEnvFiles: true }
|
||||||
);
|
);
|
||||||
|
// fix for https://github.com/nrwl/nx/issues/1666
|
||||||
|
if (process.stdin['unref']) (process.stdin as any).unref();
|
||||||
|
process.exit(status);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const yargsExecCommand: CommandModule = {
|
|||||||
builder: (yargs) => withRunOneOptions(yargs),
|
builder: (yargs) => withRunOneOptions(yargs),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
try {
|
try {
|
||||||
await (await import('./exec')).nxExecCommand(withOverrides(args));
|
await (await import('./exec')).nxExecCommand(withOverrides(args) as any);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@ -184,7 +184,7 @@ async function promptForCollection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseGeneratorString(value: string): {
|
export function parseGeneratorString(value: string): {
|
||||||
collection?: string;
|
collection?: string;
|
||||||
generator: string;
|
generator: string;
|
||||||
} {
|
} {
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import { yargsShowCommand } from './show/command-object';
|
|||||||
import { yargsWatchCommand } from './watch/command-object';
|
import { yargsWatchCommand } from './watch/command-object';
|
||||||
import { yargsWorkspaceLintCommand } from './workspace-lint/command-object';
|
import { yargsWorkspaceLintCommand } from './workspace-lint/command-object';
|
||||||
import { yargsResetCommand } from './reset/command-object';
|
import { yargsResetCommand } from './reset/command-object';
|
||||||
|
import { yargsReleaseCommand } from './release/command-object';
|
||||||
|
|
||||||
// Ensure that the output takes up the available width of the terminal.
|
// Ensure that the output takes up the available width of the terminal.
|
||||||
yargs.wrap(yargs.terminalWidth());
|
yargs.wrap(yargs.terminalWidth());
|
||||||
@ -78,6 +79,7 @@ export const commandsObject = yargs
|
|||||||
.command(yargsMigrateCommand)
|
.command(yargsMigrateCommand)
|
||||||
.command(yargsNewCommand)
|
.command(yargsNewCommand)
|
||||||
.command(yargsPrintAffectedCommand)
|
.command(yargsPrintAffectedCommand)
|
||||||
|
.command(yargsReleaseCommand)
|
||||||
.command(yargsRepairCommand)
|
.command(yargsRepairCommand)
|
||||||
.command(yargsReportCommand)
|
.command(yargsReportCommand)
|
||||||
.command(yargsResetCommand)
|
.command(yargsResetCommand)
|
||||||
|
|||||||
163
packages/nx/src/command-line/release/changelog.ts
Normal file
163
packages/nx/src/command-line/release/changelog.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import * as chalk from 'chalk';
|
||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { dirSync } from 'tmp';
|
||||||
|
import { joinPathFragments, logger, output } from '../../devkit-exports';
|
||||||
|
import { ChangelogOptions } from './command-object';
|
||||||
|
import { getGitDiff, getLastGitTag, parseCommits } from './utils/git';
|
||||||
|
import {
|
||||||
|
GithubRelease,
|
||||||
|
GithubRequestConfig,
|
||||||
|
createOrUpdateGithubRelease,
|
||||||
|
generateMarkdown,
|
||||||
|
getGitHubRemote,
|
||||||
|
getGithubReleaseByTag,
|
||||||
|
resolveGithubToken,
|
||||||
|
} from './utils/github';
|
||||||
|
import { launchEditor } from './utils/launch-editor';
|
||||||
|
import { printDiff } from './utils/print-diff';
|
||||||
|
|
||||||
|
export async function changelogHandler(args: ChangelogOptions): Promise<void> {
|
||||||
|
/**
|
||||||
|
* TODO: allow the prefix and version to be controllable via config as well once we flesh out
|
||||||
|
* changelog customization, and how it will interact with independently released projects.
|
||||||
|
*/
|
||||||
|
const tagVersionPrefix = args.tagVersionPrefix ?? 'v';
|
||||||
|
const releaseVersion = `${tagVersionPrefix}${args.version}`;
|
||||||
|
|
||||||
|
const githubRemote = getGitHubRemote(args.gitRemote);
|
||||||
|
const token = await resolveGithubToken();
|
||||||
|
const githubRequestConfig: GithubRequestConfig = {
|
||||||
|
repo: githubRemote,
|
||||||
|
token,
|
||||||
|
};
|
||||||
|
|
||||||
|
const from = args.from || (await getLastGitTag());
|
||||||
|
if (!from) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not determine the previous git tag, please provide and explicit reference using --from`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const to = args.to;
|
||||||
|
const rawCommits = await getGitDiff(from, args.to);
|
||||||
|
|
||||||
|
// Parse as conventional commits
|
||||||
|
const commits = parseCommits(rawCommits).filter((c) => {
|
||||||
|
const type = c.type;
|
||||||
|
// Always ignore non user-facing commits for now
|
||||||
|
// TODO: allow this filter to be configurable via config in a future release
|
||||||
|
if (type === 'feat' || type === 'fix' || type === 'perf') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialMarkdown = await generateMarkdown(
|
||||||
|
commits,
|
||||||
|
releaseVersion,
|
||||||
|
githubRequestConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
let finalMarkdown = initialMarkdown;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If interactive mode, make the markdown available for the user to modify in their editor of choice,
|
||||||
|
* in a similar style to git interactive rebases/merges.
|
||||||
|
*/
|
||||||
|
if (args.interactive) {
|
||||||
|
const tmpDir = dirSync().name;
|
||||||
|
const changelogPath = joinPathFragments(tmpDir, 'c.md');
|
||||||
|
writeFileSync(changelogPath, initialMarkdown);
|
||||||
|
await launchEditor(changelogPath);
|
||||||
|
finalMarkdown = readFileSync(changelogPath, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingGithubReleaseForVersion: GithubRelease;
|
||||||
|
try {
|
||||||
|
existingGithubReleaseForVersion = await getGithubReleaseByTag(
|
||||||
|
githubRequestConfig,
|
||||||
|
releaseVersion
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
output.error({
|
||||||
|
title: `Unable to resolve data via the Github API. You can use any of the following options to resolve this:`,
|
||||||
|
bodyLines: [
|
||||||
|
'- Set the `GITHUB_TOKEN` or `GH_TOKEN` environment variable to a valid Github token with `repo` scope',
|
||||||
|
'- Have an active session via the official gh CLI tool (https://cli.github.com) in your current terminal',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (err.response?.status === 404) {
|
||||||
|
// No existing release found, this is fine
|
||||||
|
} else {
|
||||||
|
// Rethrow unknown errors for now
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changesRangeText =
|
||||||
|
to === 'HEAD' ? `since ${from}` : `between ${from} and ${to}`;
|
||||||
|
|
||||||
|
if (existingGithubReleaseForVersion) {
|
||||||
|
output.log({
|
||||||
|
title: `Found existing Github release for ${chalk.white(
|
||||||
|
releaseVersion
|
||||||
|
)}, regenerating with changes ${chalk.cyan(changesRangeText)}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
output.log({
|
||||||
|
title: `Creating a new Github release for ${chalk.white(
|
||||||
|
releaseVersion
|
||||||
|
)}, including changes ${chalk.cyan(changesRangeText)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
printReleaseLog(
|
||||||
|
releaseVersion,
|
||||||
|
githubRemote,
|
||||||
|
args.dryRun,
|
||||||
|
finalMarkdown,
|
||||||
|
existingGithubReleaseForVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (args.dryRun) {
|
||||||
|
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
|
||||||
|
} else {
|
||||||
|
await createOrUpdateGithubRelease(
|
||||||
|
githubRequestConfig,
|
||||||
|
{
|
||||||
|
version: releaseVersion,
|
||||||
|
body: finalMarkdown,
|
||||||
|
},
|
||||||
|
existingGithubReleaseForVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printReleaseLog(
|
||||||
|
releaseVersion: string,
|
||||||
|
githubRemote: string,
|
||||||
|
isDryRun: boolean,
|
||||||
|
finalMarkdown: string,
|
||||||
|
existingGithubReleaseForVersion?: GithubRelease
|
||||||
|
) {
|
||||||
|
const logTitle = `https://github.com/${githubRemote}/releases/tag/${releaseVersion}`;
|
||||||
|
if (existingGithubReleaseForVersion) {
|
||||||
|
console.error(
|
||||||
|
`${chalk.white('UPDATE')} ${logTitle}${
|
||||||
|
isDryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`${chalk.green('CREATE')} ${logTitle}${
|
||||||
|
isDryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
printDiff('', finalMarkdown);
|
||||||
|
}
|
||||||
186
packages/nx/src/command-line/release/command-object.ts
Normal file
186
packages/nx/src/command-line/release/command-object.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { CommandModule, showHelp } from 'yargs';
|
||||||
|
import { readNxJson } from '../../project-graph/file-utils';
|
||||||
|
import {
|
||||||
|
parseCSV,
|
||||||
|
RunManyOptions,
|
||||||
|
withOverrides,
|
||||||
|
withRunManyOptions,
|
||||||
|
} from '../yargs-utils/shared-options';
|
||||||
|
|
||||||
|
export interface NxReleaseArgs {
|
||||||
|
groups?: string[];
|
||||||
|
projects?: string[];
|
||||||
|
dryRun?: boolean;
|
||||||
|
verbose?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VersionOptions = NxReleaseArgs & {
|
||||||
|
specifier?: string;
|
||||||
|
preid?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChangelogOptions = NxReleaseArgs & {
|
||||||
|
version: string;
|
||||||
|
to: string;
|
||||||
|
from?: string;
|
||||||
|
interactive?: boolean;
|
||||||
|
gitRemote?: string;
|
||||||
|
tagVersionPrefix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PublishOptions = NxReleaseArgs &
|
||||||
|
RunManyOptions & {
|
||||||
|
registry?: string;
|
||||||
|
tag?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const yargsReleaseCommand: CommandModule<
|
||||||
|
Record<string, unknown>,
|
||||||
|
NxReleaseArgs
|
||||||
|
> = {
|
||||||
|
command: 'release',
|
||||||
|
describe:
|
||||||
|
'**ALPHA**: Orchestrate versioning and publishing of applications and libraries',
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
.command(versionCommand)
|
||||||
|
.command(changelogCommand)
|
||||||
|
.command(publishCommand)
|
||||||
|
.demandCommand()
|
||||||
|
.option('groups', {
|
||||||
|
description:
|
||||||
|
'One or more release groups to target with the current command.',
|
||||||
|
type: 'string',
|
||||||
|
coerce: parseCSV,
|
||||||
|
alias: ['group', 'g'],
|
||||||
|
})
|
||||||
|
.option('projects', {
|
||||||
|
type: 'string',
|
||||||
|
alias: 'p',
|
||||||
|
coerce: parseCSV,
|
||||||
|
describe:
|
||||||
|
'Projects to run. (comma/space delimited project names and/or patterns)',
|
||||||
|
})
|
||||||
|
.option('dryRun', {
|
||||||
|
describe:
|
||||||
|
'Preview the changes without updating files/creating releases',
|
||||||
|
alias: 'd',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
.option('verbose', {
|
||||||
|
type: 'boolean',
|
||||||
|
describe:
|
||||||
|
'Prints additional information about the commands (e.g., stack traces)',
|
||||||
|
})
|
||||||
|
.check((argv) => {
|
||||||
|
if (argv.groups && argv.projects) {
|
||||||
|
throw new Error(
|
||||||
|
'The --projects and --groups options are mutually exclusive, please use one or the other.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const nxJson = readNxJson();
|
||||||
|
if (argv.groups?.length) {
|
||||||
|
for (const group of argv.groups) {
|
||||||
|
if (!nxJson.release?.groups?.[group]) {
|
||||||
|
throw new Error(
|
||||||
|
`The specified release group "${group}" was not found in nx.json`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}) as any, // the type: 'string' and coerce: parseCSV combo isn't enough to produce the string[] type for projects and groups
|
||||||
|
handler: async () => {
|
||||||
|
showHelp();
|
||||||
|
process.exit(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionCommand: CommandModule<NxReleaseArgs, VersionOptions> = {
|
||||||
|
command: 'version [specifier]',
|
||||||
|
aliases: ['v'],
|
||||||
|
describe:
|
||||||
|
'Create a version and release for one or more applications and libraries',
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
.positional('specifier', {
|
||||||
|
type: 'string',
|
||||||
|
describe:
|
||||||
|
'Exact version or semver keyword to apply to the selected release group.',
|
||||||
|
})
|
||||||
|
.option('preid', {
|
||||||
|
type: 'string',
|
||||||
|
describe:
|
||||||
|
'The optional prerelease identifier to apply to the version, in the case that specifier has been set to prerelease.',
|
||||||
|
default: '',
|
||||||
|
}),
|
||||||
|
handler: (args) => import('./version').then((m) => m.versionHandler(args)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const changelogCommand: CommandModule<NxReleaseArgs, ChangelogOptions> = {
|
||||||
|
command: 'changelog [version]',
|
||||||
|
aliases: ['c'],
|
||||||
|
describe:
|
||||||
|
'Generate a changelog for one or more projects, and optionally push to Github',
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
// Disable default meaning of yargs version for this command
|
||||||
|
.version(false)
|
||||||
|
.positional('version', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The version to create a Github release and changelog for',
|
||||||
|
})
|
||||||
|
.option('from', {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'The git reference to use as the start of the changelog. If not set it will attempt to resolve the latest tag and use that',
|
||||||
|
})
|
||||||
|
.option('to', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The git reference to use as the end of the changelog',
|
||||||
|
default: 'HEAD',
|
||||||
|
})
|
||||||
|
.option('interactive', {
|
||||||
|
alias: 'i',
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
.option('gitRemote', {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Alternate git remote in the form {user}/{repo} on which to create the Github release (useful for testing)',
|
||||||
|
default: 'origin',
|
||||||
|
})
|
||||||
|
.option('tagVersionPrefix', {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Prefix to apply to the version when creating the Github release tag',
|
||||||
|
default: 'v',
|
||||||
|
})
|
||||||
|
.check((argv) => {
|
||||||
|
if (!argv.version) {
|
||||||
|
throw new Error('A target version must be specified');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
handler: (args) =>
|
||||||
|
import('./changelog').then((m) => m.changelogHandler(args)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishCommand: CommandModule<NxReleaseArgs, PublishOptions> = {
|
||||||
|
command: 'publish',
|
||||||
|
aliases: ['p'],
|
||||||
|
describe: 'Publish a versioned project to a registry',
|
||||||
|
builder: (yargs) =>
|
||||||
|
withRunManyOptions(yargs)
|
||||||
|
.option('registry', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The registry to publish to',
|
||||||
|
})
|
||||||
|
.option('tag', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The distribution tag to apply to the published package',
|
||||||
|
}),
|
||||||
|
handler: (args) =>
|
||||||
|
import('./publish').then((m) => m.publishHandler(withOverrides(args, 2))),
|
||||||
|
};
|
||||||
48
packages/nx/src/command-line/release/config/config.spec.ts
Normal file
48
packages/nx/src/command-line/release/config/config.spec.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { createNxReleaseConfig } from './config';
|
||||||
|
|
||||||
|
describe('createNxReleaseConfig()', () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: undefined,
|
||||||
|
output: {
|
||||||
|
groups: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {},
|
||||||
|
output: {
|
||||||
|
groups: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
groups: {},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
groups: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
groups: {
|
||||||
|
foo: {
|
||||||
|
projects: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
groups: {
|
||||||
|
foo: {
|
||||||
|
projects: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((c, i) => {
|
||||||
|
it(`should create appropriate NxReleaseConfig, CASE: ${i}`, () => {
|
||||||
|
expect(createNxReleaseConfig(c.input)).toEqual(c.output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
packages/nx/src/command-line/release/config/config.ts
Normal file
12
packages/nx/src/command-line/release/config/config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NxJsonConfiguration } from '../../../config/nx-json';
|
||||||
|
|
||||||
|
// Apply default configuration to any optional user configuration
|
||||||
|
export function createNxReleaseConfig(
|
||||||
|
userConfig: NxJsonConfiguration['release'] = {}
|
||||||
|
): Required<NxJsonConfiguration['release']> {
|
||||||
|
const nxReleaseConfig: Required<NxJsonConfiguration['release']> = {
|
||||||
|
...userConfig,
|
||||||
|
groups: userConfig.groups || {},
|
||||||
|
};
|
||||||
|
return nxReleaseConfig;
|
||||||
|
}
|
||||||
@ -0,0 +1,249 @@
|
|||||||
|
import { ProjectGraph } from '../../../config/project-graph';
|
||||||
|
import { createReleaseGroups } from './create-release-groups';
|
||||||
|
|
||||||
|
describe('create-release-groups', () => {
|
||||||
|
let projectGraph: ProjectGraph;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
projectGraph = {
|
||||||
|
nodes: {
|
||||||
|
'lib-a': {
|
||||||
|
name: 'lib-a',
|
||||||
|
type: 'lib',
|
||||||
|
data: {
|
||||||
|
root: 'libs/lib-a',
|
||||||
|
targets: {
|
||||||
|
'nx-release-publish': {},
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
|
'lib-b': {
|
||||||
|
name: 'lib-b',
|
||||||
|
type: 'lib',
|
||||||
|
data: {
|
||||||
|
root: 'libs/lib-b',
|
||||||
|
targets: {
|
||||||
|
'nx-release-publish': {},
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dependencies: {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no user specified groups', () => {
|
||||||
|
it('should return a catch all release group containing all projects when no groups are specified', async () => {
|
||||||
|
const res = await createReleaseGroups(projectGraph, {});
|
||||||
|
expect(res).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"error": null,
|
||||||
|
"releaseGroups": [
|
||||||
|
{
|
||||||
|
"name": "__default__",
|
||||||
|
"projects": [
|
||||||
|
"lib-a",
|
||||||
|
"lib-b",
|
||||||
|
],
|
||||||
|
"version": {
|
||||||
|
"generator": "@nx/js:release-version",
|
||||||
|
"generatorOptions": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('user specified groups', () => {
|
||||||
|
it('should ignore any projects not matched to user specified groups', async () => {
|
||||||
|
const res = await createReleaseGroups(projectGraph, {
|
||||||
|
'group-1': {
|
||||||
|
projects: ['lib-a'], // intentionally no lib-b, so it should be ignored
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"error": null,
|
||||||
|
"releaseGroups": [
|
||||||
|
{
|
||||||
|
"name": "group-1",
|
||||||
|
"projects": [
|
||||||
|
"lib-a",
|
||||||
|
],
|
||||||
|
"version": {
|
||||||
|
"generator": "@nx/js:release-version",
|
||||||
|
"generatorOptions": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect user overrides for "version" config', async () => {
|
||||||
|
const res = await createReleaseGroups(projectGraph, {
|
||||||
|
'group-1': {
|
||||||
|
projects: ['lib-a'],
|
||||||
|
version: {
|
||||||
|
generator: '@custom/generator',
|
||||||
|
generatorOptions: {
|
||||||
|
optionsOverride: 'something',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'group-2': {
|
||||||
|
projects: ['lib-b'],
|
||||||
|
version: {
|
||||||
|
generator: '@custom/generator-alternative',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"error": null,
|
||||||
|
"releaseGroups": [
|
||||||
|
{
|
||||||
|
"name": "group-1",
|
||||||
|
"projects": [
|
||||||
|
"lib-a",
|
||||||
|
],
|
||||||
|
"version": {
|
||||||
|
"generator": "@custom/generator",
|
||||||
|
"generatorOptions": {
|
||||||
|
"optionsOverride": "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "group-2",
|
||||||
|
"projects": [
|
||||||
|
"lib-b",
|
||||||
|
],
|
||||||
|
"version": {
|
||||||
|
"generator": "@custom/generator-alternative",
|
||||||
|
"generatorOptions": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('release group config errors', () => {
|
||||||
|
it('should return an error if a project matches multiple groups', async () => {
|
||||||
|
const res = await createReleaseGroups(projectGraph, {
|
||||||
|
'group-1': {
|
||||||
|
projects: ['lib-a'],
|
||||||
|
},
|
||||||
|
'group-2': {
|
||||||
|
projects: ['lib-a'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "PROJECT_MATCHES_MULTIPLE_GROUPS",
|
||||||
|
"data": {
|
||||||
|
"project": "lib-a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"releaseGroups": [],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error if no projects can be resolved for a group', async () => {
|
||||||
|
const res = await createReleaseGroups(projectGraph, {
|
||||||
|
'group-1': {
|
||||||
|
projects: ['lib-does-not-exist'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "RELEASE_GROUP_MATCHES_NO_PROJECTS",
|
||||||
|
"data": {
|
||||||
|
"releaseGroupName": "group-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"releaseGroups": [],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error if any matched projects do not have the required target specified', async () => {
|
||||||
|
const res = await createReleaseGroups(
|
||||||
|
{
|
||||||
|
...projectGraph,
|
||||||
|
nodes: {
|
||||||
|
...projectGraph.nodes,
|
||||||
|
'project-without-target': {
|
||||||
|
name: 'project-without-target',
|
||||||
|
type: 'lib',
|
||||||
|
data: {
|
||||||
|
root: 'libs/project-without-target',
|
||||||
|
targets: {},
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'group-1': {
|
||||||
|
projects: '*', // using string form to ensure that is supported in addition to array form
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'nx-release-publish'
|
||||||
|
);
|
||||||
|
expect(res).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "PROJECTS_MISSING_TARGET",
|
||||||
|
"data": {
|
||||||
|
"projects": [
|
||||||
|
"project-without-target",
|
||||||
|
],
|
||||||
|
"targetName": "nx-release-publish",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"releaseGroups": [],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const res2 = await createReleaseGroups(
|
||||||
|
{
|
||||||
|
...projectGraph,
|
||||||
|
nodes: {
|
||||||
|
...projectGraph.nodes,
|
||||||
|
'another-project-without-target': {
|
||||||
|
name: 'another-project-without-target',
|
||||||
|
type: 'lib',
|
||||||
|
data: {
|
||||||
|
root: 'libs/another-project-without-target',
|
||||||
|
targets: {},
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
'nx-release-publish'
|
||||||
|
);
|
||||||
|
expect(res2).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "PROJECTS_MISSING_TARGET",
|
||||||
|
"data": {
|
||||||
|
"projects": [
|
||||||
|
"another-project-without-target",
|
||||||
|
],
|
||||||
|
"targetName": "nx-release-publish",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"releaseGroups": [],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,225 @@
|
|||||||
|
import type { NxJsonConfiguration } from '../../../config/nx-json';
|
||||||
|
import { output, type ProjectGraph } from '../../../devkit-exports';
|
||||||
|
import { findMatchingProjects } from '../../../utils/find-matching-projects';
|
||||||
|
import { projectHasTarget } from '../../../utils/project-graph-utils';
|
||||||
|
import { resolveNxJsonConfigErrorMessage } from '../utils/resolve-nx-json-error-message';
|
||||||
|
|
||||||
|
export interface ReleaseGroup {
|
||||||
|
name: string;
|
||||||
|
projects: string[];
|
||||||
|
version: {
|
||||||
|
generator: string;
|
||||||
|
generatorOptions: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// We explicitly handle some expected errors in order to provide the best possible DX
|
||||||
|
interface CreateReleaseGroupsError {
|
||||||
|
code:
|
||||||
|
| 'RELEASE_GROUP_MATCHES_NO_PROJECTS'
|
||||||
|
| 'PROJECT_MATCHES_MULTIPLE_GROUPS'
|
||||||
|
| 'PROJECTS_MISSING_TARGET';
|
||||||
|
data: Record<string, string | string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CATCH_ALL_RELEASE_GROUP = '__default__';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a set of release groups based on the relevant user specified config ready
|
||||||
|
* to be consumed by the release commands.
|
||||||
|
*/
|
||||||
|
export async function createReleaseGroups(
|
||||||
|
projectGraph: ProjectGraph,
|
||||||
|
userSpecifiedGroups: NxJsonConfiguration['release']['groups'] = {},
|
||||||
|
requiredTargetName?: 'nx-release-publish'
|
||||||
|
): Promise<{
|
||||||
|
error: null | CreateReleaseGroupsError;
|
||||||
|
releaseGroups: ReleaseGroup[];
|
||||||
|
}> {
|
||||||
|
const DEFAULT_VERSION_GENERATOR = '@nx/js:release-version';
|
||||||
|
const DEFAULT_VERSION_GENERATOR_OPTIONS = {};
|
||||||
|
|
||||||
|
const allProjects = findMatchingProjects(['*'], projectGraph.nodes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No user specified release groups, so we treat all projects as being in one release group
|
||||||
|
* together in which all projects are released in lock step.
|
||||||
|
*/
|
||||||
|
if (Object.keys(userSpecifiedGroups).length === 0) {
|
||||||
|
// Ensure all projects have the relevant target available, if applicable
|
||||||
|
if (requiredTargetName) {
|
||||||
|
const error = ensureProjectsHaveTarget(
|
||||||
|
allProjects,
|
||||||
|
projectGraph,
|
||||||
|
requiredTargetName
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
return {
|
||||||
|
error,
|
||||||
|
releaseGroups: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
releaseGroups: [
|
||||||
|
{
|
||||||
|
name: CATCH_ALL_RELEASE_GROUP,
|
||||||
|
projects: allProjects,
|
||||||
|
version: {
|
||||||
|
generator: DEFAULT_VERSION_GENERATOR,
|
||||||
|
generatorOptions: DEFAULT_VERSION_GENERATOR_OPTIONS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has specified at least one release group.
|
||||||
|
*
|
||||||
|
* Resolve all the project names into their release groups, and check
|
||||||
|
* that individual projects are not found in multiple groups.
|
||||||
|
*/
|
||||||
|
const releaseGroups: ReleaseGroup[] = [];
|
||||||
|
const alreadyMatchedProjects = new Set<string>();
|
||||||
|
|
||||||
|
for (const [releaseGroupName, userSpecifiedGroup] of Object.entries(
|
||||||
|
userSpecifiedGroups
|
||||||
|
)) {
|
||||||
|
// Ensure that the user config for the release group can resolve at least one project
|
||||||
|
const matchingProjects = findMatchingProjects(
|
||||||
|
Array.isArray(userSpecifiedGroup.projects)
|
||||||
|
? userSpecifiedGroup.projects
|
||||||
|
: [userSpecifiedGroup.projects],
|
||||||
|
projectGraph.nodes
|
||||||
|
);
|
||||||
|
if (!matchingProjects.length) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
code: 'RELEASE_GROUP_MATCHES_NO_PROJECTS',
|
||||||
|
data: {
|
||||||
|
releaseGroupName: releaseGroupName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
releaseGroups: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all matching projects have the relevant target available, if applicable
|
||||||
|
if (requiredTargetName) {
|
||||||
|
const error = ensureProjectsHaveTarget(
|
||||||
|
matchingProjects,
|
||||||
|
projectGraph,
|
||||||
|
requiredTargetName
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
return {
|
||||||
|
error,
|
||||||
|
releaseGroups: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const project of matchingProjects) {
|
||||||
|
if (alreadyMatchedProjects.has(project)) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
code: 'PROJECT_MATCHES_MULTIPLE_GROUPS',
|
||||||
|
data: {
|
||||||
|
project,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
releaseGroups: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
alreadyMatchedProjects.add(project);
|
||||||
|
}
|
||||||
|
releaseGroups.push({
|
||||||
|
name: releaseGroupName,
|
||||||
|
projects: matchingProjects,
|
||||||
|
version: userSpecifiedGroup.version
|
||||||
|
? {
|
||||||
|
generator:
|
||||||
|
userSpecifiedGroup.version.generator || DEFAULT_VERSION_GENERATOR,
|
||||||
|
generatorOptions:
|
||||||
|
userSpecifiedGroup.version.generatorOptions ||
|
||||||
|
DEFAULT_VERSION_GENERATOR_OPTIONS,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
generator: DEFAULT_VERSION_GENERATOR,
|
||||||
|
generatorOptions: DEFAULT_VERSION_GENERATOR_OPTIONS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
releaseGroups,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleCreateReleaseGroupsError(
|
||||||
|
error: CreateReleaseGroupsError
|
||||||
|
) {
|
||||||
|
switch (error.code) {
|
||||||
|
case 'RELEASE_GROUP_MATCHES_NO_PROJECTS':
|
||||||
|
{
|
||||||
|
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
|
||||||
|
'release',
|
||||||
|
'groups',
|
||||||
|
]);
|
||||||
|
output.error({
|
||||||
|
title: `Release group "${error.data.releaseGroupName}" matches no projects. Please ensure all release groups match at least one project:`,
|
||||||
|
bodyLines: [nxJsonMessage],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'PROJECT_MATCHES_MULTIPLE_GROUPS':
|
||||||
|
{
|
||||||
|
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
|
||||||
|
'release',
|
||||||
|
'groups',
|
||||||
|
]);
|
||||||
|
output.error({
|
||||||
|
title: `Project "${error.data.project}" matches multiple release groups. Please ensure all projects are part of only one release group:`,
|
||||||
|
bodyLines: [nxJsonMessage],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'PROJECTS_MISSING_TARGET':
|
||||||
|
{
|
||||||
|
output.error({
|
||||||
|
title: `Based on your config, the following projects were matched for release but do not have a "${error.data.targetName}" target specified. Please ensure you have an appropriate plugin such as @nx/js installed, or have configured the target manually, or exclude the projects using release groups config in nx.json:`,
|
||||||
|
bodyLines: Array.from(error.data.projects).map((name) => `- ${name}`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled error code: ${error.code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureProjectsHaveTarget(
|
||||||
|
projects: string[],
|
||||||
|
projectGraph: ProjectGraph,
|
||||||
|
requiredTargetName: string
|
||||||
|
): null | CreateReleaseGroupsError {
|
||||||
|
const missingTargetProjects = projects.filter(
|
||||||
|
(project) =>
|
||||||
|
!projectHasTarget(projectGraph.nodes[project], requiredTargetName)
|
||||||
|
);
|
||||||
|
if (missingTargetProjects.length) {
|
||||||
|
return {
|
||||||
|
code: 'PROJECTS_MISSING_TARGET',
|
||||||
|
data: {
|
||||||
|
targetName: requiredTargetName,
|
||||||
|
projects: missingTargetProjects,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
222
packages/nx/src/command-line/release/publish.ts
Normal file
222
packages/nx/src/command-line/release/publish.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import { readNxJson } from '../../config/nx-json';
|
||||||
|
import {
|
||||||
|
ProjectGraph,
|
||||||
|
ProjectGraphProjectNode,
|
||||||
|
} from '../../config/project-graph';
|
||||||
|
import { NxJsonConfiguration, output } from '../../devkit-exports';
|
||||||
|
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
||||||
|
import { runCommand } from '../../tasks-runner/run-command';
|
||||||
|
import {
|
||||||
|
createOverrides,
|
||||||
|
readGraphFileFromGraphArg,
|
||||||
|
} from '../../utils/command-line-utils';
|
||||||
|
import { findMatchingProjects } from '../../utils/find-matching-projects';
|
||||||
|
import { PublishOptions } from './command-object';
|
||||||
|
import { createNxReleaseConfig } from './config/config';
|
||||||
|
import {
|
||||||
|
CATCH_ALL_RELEASE_GROUP,
|
||||||
|
ReleaseGroup,
|
||||||
|
createReleaseGroups,
|
||||||
|
handleCreateReleaseGroupsError,
|
||||||
|
} from './config/create-release-groups';
|
||||||
|
import { generateGraph } from '../graph/graph';
|
||||||
|
|
||||||
|
export async function publishHandler(
|
||||||
|
args: PublishOptions & { __overrides_unparsed__: string[] }
|
||||||
|
): Promise<void> {
|
||||||
|
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
||||||
|
const nxJson = readNxJson();
|
||||||
|
|
||||||
|
// Apply default configuration to any optional user configuration
|
||||||
|
const nxReleaseConfig = createNxReleaseConfig(nxJson.release);
|
||||||
|
const releaseGroupsData = await createReleaseGroups(
|
||||||
|
projectGraph,
|
||||||
|
nxReleaseConfig.groups
|
||||||
|
);
|
||||||
|
if (releaseGroupsData.error) {
|
||||||
|
return await handleCreateReleaseGroupsError(releaseGroupsData.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let { releaseGroups } = releaseGroupsData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User is filtering to a subset of projects. We need to make sure that what they have provided can be reconciled
|
||||||
|
* against their configuration in terms of release groups and the ungroupedProjectsHandling option.
|
||||||
|
*/
|
||||||
|
if (args.projects?.length) {
|
||||||
|
const matchingProjectsForFilter = findMatchingProjects(
|
||||||
|
args.projects,
|
||||||
|
projectGraph.nodes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingProjectsForFilter.length) {
|
||||||
|
output.error({
|
||||||
|
title: `Your --projects filter "${args.projects}" did not match any projects in the workspace`,
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredProjectToReleaseGroup = new Map<string, ReleaseGroup>();
|
||||||
|
const releaseGroupToFilteredProjects = new Map<ReleaseGroup, Set<string>>();
|
||||||
|
|
||||||
|
// Figure out which release groups, if any, that the filtered projects belong to so that we can resolve other config
|
||||||
|
for (const releaseGroup of releaseGroups) {
|
||||||
|
const matchingProjectsForReleaseGroup = findMatchingProjects(
|
||||||
|
releaseGroup.projects,
|
||||||
|
projectGraph.nodes
|
||||||
|
);
|
||||||
|
for (const matchingProject of matchingProjectsForFilter) {
|
||||||
|
if (matchingProjectsForReleaseGroup.includes(matchingProject)) {
|
||||||
|
filteredProjectToReleaseGroup.set(matchingProject, releaseGroup);
|
||||||
|
if (!releaseGroupToFilteredProjects.has(releaseGroup)) {
|
||||||
|
releaseGroupToFilteredProjects.set(releaseGroup, new Set());
|
||||||
|
}
|
||||||
|
releaseGroupToFilteredProjects.get(releaseGroup).add(matchingProject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there are release groups specified, each filtered project must match at least one release
|
||||||
|
* group, otherwise the command + config combination is invalid.
|
||||||
|
*/
|
||||||
|
if (Object.keys(nxReleaseConfig.groups).length) {
|
||||||
|
const unmatchedProjects = matchingProjectsForFilter.filter(
|
||||||
|
(p) => !filteredProjectToReleaseGroup.has(p)
|
||||||
|
);
|
||||||
|
if (unmatchedProjects.length) {
|
||||||
|
output.error({
|
||||||
|
title: `The following projects which match your projects filter "${args.projects}" did not match any configured release groups:`,
|
||||||
|
bodyLines: unmatchedProjects.map((p) => `- ${p}`),
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.note({
|
||||||
|
title: `Your filter "${args.projects}" matched the following projects:`,
|
||||||
|
bodyLines: matchingProjectsForFilter.map((p) => {
|
||||||
|
const releaseGroupForProject = filteredProjectToReleaseGroup.get(p);
|
||||||
|
if (releaseGroupForProject.name === CATCH_ALL_RELEASE_GROUP) {
|
||||||
|
return `- ${p}`;
|
||||||
|
}
|
||||||
|
return `- ${p} (release group "${releaseGroupForProject.name}")`;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter the releaseGroups collection appropriately
|
||||||
|
releaseGroups = releaseGroups.filter((rg) =>
|
||||||
|
releaseGroupToFilteredProjects.has(rg)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run publishing for all remaining release groups and filtered projects within them
|
||||||
|
*/
|
||||||
|
for (const releaseGroup of releaseGroups) {
|
||||||
|
await runPublishOnProjects(
|
||||||
|
args,
|
||||||
|
projectGraph,
|
||||||
|
nxJson,
|
||||||
|
Array.from(releaseGroupToFilteredProjects.get(releaseGroup))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user is filtering by release group
|
||||||
|
*/
|
||||||
|
if (args.groups?.length) {
|
||||||
|
releaseGroups = releaseGroups.filter((g) => args.groups?.includes(g.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be an impossible state, as we should have explicitly handled any errors/invalid config by now
|
||||||
|
if (!releaseGroups.length) {
|
||||||
|
output.error({
|
||||||
|
title: `No projects could be matched for versioning, please report this case and include your nx.json config`,
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run publishing for all remaining release groups
|
||||||
|
*/
|
||||||
|
for (const releaseGroup of releaseGroups) {
|
||||||
|
await runPublishOnProjects(
|
||||||
|
args,
|
||||||
|
projectGraph,
|
||||||
|
nxJson,
|
||||||
|
releaseGroup.projects
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPublishOnProjects(
|
||||||
|
args: PublishOptions & { __overrides_unparsed__: string[] },
|
||||||
|
projectGraph: ProjectGraph,
|
||||||
|
nxJson: NxJsonConfiguration,
|
||||||
|
projectNames: string[]
|
||||||
|
) {
|
||||||
|
const projectsToRun: ProjectGraphProjectNode[] = projectNames.map(
|
||||||
|
(projectName) => projectGraph.nodes[projectName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const overrides = createOverrides(args.__overrides_unparsed__);
|
||||||
|
|
||||||
|
if (args.registry) {
|
||||||
|
overrides.registry = args.registry;
|
||||||
|
}
|
||||||
|
if (args.tag) {
|
||||||
|
overrides.tag = args.tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.verbose) {
|
||||||
|
process.env.NX_VERBOSE_LOGGING = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = ['nx-release-publish'];
|
||||||
|
|
||||||
|
if (args.graph) {
|
||||||
|
const file = readGraphFileFromGraphArg(args);
|
||||||
|
const projectNames = projectsToRun.map((t) => t.name);
|
||||||
|
return await generateGraph(
|
||||||
|
{
|
||||||
|
watch: false,
|
||||||
|
all: false,
|
||||||
|
open: true,
|
||||||
|
view: 'tasks',
|
||||||
|
targets,
|
||||||
|
projects: projectNames,
|
||||||
|
file,
|
||||||
|
},
|
||||||
|
projectNames
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Run the relevant nx-release-publish executor on each of the selected projects.
|
||||||
|
*/
|
||||||
|
const status = await runCommand(
|
||||||
|
projectsToRun,
|
||||||
|
projectGraph,
|
||||||
|
{ nxJson },
|
||||||
|
{
|
||||||
|
targets,
|
||||||
|
outputStyle: 'static',
|
||||||
|
...(args as any),
|
||||||
|
},
|
||||||
|
overrides,
|
||||||
|
null,
|
||||||
|
{},
|
||||||
|
{ excludeTaskDependencies: false, loadDotEnvFiles: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status !== 0) {
|
||||||
|
// fix for https://github.com/nrwl/nx/issues/1666
|
||||||
|
if (process.stdin['unref']) (process.stdin as any).unref();
|
||||||
|
process.exit(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
158
packages/nx/src/command-line/release/utils/git.ts
Normal file
158
packages/nx/src/command-line/release/utils/git.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* Special thanks to changelogen for the original inspiration for many of these utilities:
|
||||||
|
* https://github.com/unjs/changelogen
|
||||||
|
*/
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
|
||||||
|
export interface GitCommitAuthor {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawGitCommit {
|
||||||
|
message: string;
|
||||||
|
body: string;
|
||||||
|
shortHash: string;
|
||||||
|
author: GitCommitAuthor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Reference {
|
||||||
|
type: 'hash' | 'issue' | 'pull-request';
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitCommit extends RawGitCommit {
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
scope: string;
|
||||||
|
references: Reference[];
|
||||||
|
authors: GitCommitAuthor[];
|
||||||
|
isBreaking: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLastGitTag() {
|
||||||
|
const r = await execCommand('git', ['describe', '--tags', '--abbrev=0'])
|
||||||
|
.then((r) => r.split('\n').filter(Boolean))
|
||||||
|
.catch(() => []);
|
||||||
|
return r.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGitDiff(
|
||||||
|
from: string | undefined,
|
||||||
|
to = 'HEAD'
|
||||||
|
): Promise<RawGitCommit[]> {
|
||||||
|
// https://git-scm.com/docs/pretty-formats
|
||||||
|
const r = await execCommand('git', [
|
||||||
|
'--no-pager',
|
||||||
|
'log',
|
||||||
|
`${from ? `${from}...` : ''}${to}`,
|
||||||
|
'--pretty="----%n%s|%h|%an|%ae%n%b"',
|
||||||
|
'--name-status',
|
||||||
|
]);
|
||||||
|
return r
|
||||||
|
.split('----\n')
|
||||||
|
.splice(1)
|
||||||
|
.map((line) => {
|
||||||
|
const [firstLine, ..._body] = line.split('\n');
|
||||||
|
const [message, shortHash, authorName, authorEmail] =
|
||||||
|
firstLine.split('|');
|
||||||
|
const r: RawGitCommit = {
|
||||||
|
message,
|
||||||
|
shortHash,
|
||||||
|
author: { name: authorName, email: authorEmail },
|
||||||
|
body: _body.join('\n'),
|
||||||
|
};
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCommits(commits: RawGitCommit[]): GitCommit[] {
|
||||||
|
return commits.map((commit) => parseGitCommit(commit)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.conventionalcommits.org/en/v1.0.0/
|
||||||
|
// https://regex101.com/r/FSfNvA/1
|
||||||
|
const ConventionalCommitRegex =
|
||||||
|
/(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
|
||||||
|
const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gim;
|
||||||
|
const PullRequestRE = /\([ a-z]*(#\d+)\s*\)/gm;
|
||||||
|
const IssueRE = /(#\d+)/gm;
|
||||||
|
|
||||||
|
export function parseGitCommit(commit: RawGitCommit): GitCommit | null {
|
||||||
|
const match = commit.message.match(ConventionalCommitRegex);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = match.groups.type;
|
||||||
|
|
||||||
|
const scope = match.groups.scope || '';
|
||||||
|
|
||||||
|
const isBreaking = Boolean(match.groups.breaking);
|
||||||
|
let description = match.groups.description;
|
||||||
|
|
||||||
|
// Extract references from message
|
||||||
|
const references: Reference[] = [];
|
||||||
|
for (const m of description.matchAll(PullRequestRE)) {
|
||||||
|
references.push({ type: 'pull-request', value: m[1] });
|
||||||
|
}
|
||||||
|
for (const m of description.matchAll(IssueRE)) {
|
||||||
|
if (!references.some((i) => i.value === m[1])) {
|
||||||
|
references.push({ type: 'issue', value: m[1] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
references.push({ value: commit.shortHash, type: 'hash' });
|
||||||
|
|
||||||
|
// Remove references and normalize
|
||||||
|
description = description.replace(PullRequestRE, '').trim();
|
||||||
|
|
||||||
|
// Find all authors
|
||||||
|
const authors: GitCommitAuthor[] = [commit.author];
|
||||||
|
for (const match of commit.body.matchAll(CoAuthoredByRegex)) {
|
||||||
|
authors.push({
|
||||||
|
name: (match.groups.name || '').trim(),
|
||||||
|
email: (match.groups.email || '').trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...commit,
|
||||||
|
authors,
|
||||||
|
description,
|
||||||
|
type,
|
||||||
|
scope,
|
||||||
|
references,
|
||||||
|
isBreaking,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execCommand(
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
options?: any
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(cmd, args, {
|
||||||
|
...options,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
child.stdout.on('data', (chunk) => {
|
||||||
|
stdout += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`Command failed with exit code ${code}`));
|
||||||
|
} else {
|
||||||
|
resolve(stdout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
391
packages/nx/src/command-line/release/utils/github.ts
Normal file
391
packages/nx/src/command-line/release/utils/github.ts
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
/**
|
||||||
|
* Special thanks to changelogen for the original inspiration for many of these utilities:
|
||||||
|
* https://github.com/unjs/changelogen
|
||||||
|
*/
|
||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
import * as chalk from 'chalk';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { existsSync, promises as fsp } from 'node:fs';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { joinPathFragments, output } from '../../../devkit-exports';
|
||||||
|
import { GitCommit, Reference } from './git';
|
||||||
|
|
||||||
|
// axios types and values don't seem to match
|
||||||
|
import _axios = require('axios');
|
||||||
|
const axios = _axios as any as typeof _axios['default'];
|
||||||
|
|
||||||
|
export interface GithubRequestConfig {
|
||||||
|
repo: string;
|
||||||
|
token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GithubRelease {
|
||||||
|
id?: string;
|
||||||
|
tag_name: string;
|
||||||
|
name?: string;
|
||||||
|
body?: string;
|
||||||
|
draft?: boolean;
|
||||||
|
prerelease?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGitHubRemote(remoteName = 'origin') {
|
||||||
|
try {
|
||||||
|
const remoteUrl = execSync(`git remote get-url ${remoteName}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
// Extract the 'user/repo' part from the URL
|
||||||
|
const regex = /github\.com[/:]([\w-]+\/[\w-]+)\.git/;
|
||||||
|
const match = remoteUrl.match(regex);
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1];
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Could not extract "user/repo" data from the resolved remote URL: ${remoteUrl}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting GitHub remote:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createOrUpdateGithubRelease(
|
||||||
|
githubRequestConfig: GithubRequestConfig,
|
||||||
|
release: { version: string; body: string },
|
||||||
|
existingGithubReleaseForVersion?: GithubRelease
|
||||||
|
) {
|
||||||
|
const result = await syncGithubRelease(
|
||||||
|
githubRequestConfig,
|
||||||
|
release,
|
||||||
|
existingGithubReleaseForVersion
|
||||||
|
);
|
||||||
|
/**
|
||||||
|
* If something went wrong POSTing to Github we can still pre-populate the web form on github.com
|
||||||
|
* to allow the user to manually complete the release.
|
||||||
|
*/
|
||||||
|
if (result.status === 'manual') {
|
||||||
|
if (result.error) {
|
||||||
|
console.error(result.error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
const open = require('open');
|
||||||
|
await open(result.url)
|
||||||
|
.then(() => {
|
||||||
|
console.info(
|
||||||
|
`Follow up in the browser to manually create the release.`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.info(
|
||||||
|
`Open this link to manually create a release: \n` +
|
||||||
|
chalk.underline(chalk.cyan(result.url)) +
|
||||||
|
'\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
output.success({
|
||||||
|
title: `Successfully ${
|
||||||
|
existingGithubReleaseForVersion ? 'updated' : 'created'
|
||||||
|
} release ${chalk.bold(release.version)} on Github:`,
|
||||||
|
bodyLines: [result.url],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: allow this to be configurable via config in a future release
|
||||||
|
export async function generateMarkdown(
|
||||||
|
commits: GitCommit[],
|
||||||
|
releaseVersion: string,
|
||||||
|
githubRequestConfig: GithubRequestConfig
|
||||||
|
) {
|
||||||
|
const typeGroups = groupBy(commits, 'type');
|
||||||
|
|
||||||
|
const markdown: string[] = [];
|
||||||
|
const breakingChanges = [];
|
||||||
|
|
||||||
|
const commitTypes = {
|
||||||
|
feat: { title: '🚀 Features' },
|
||||||
|
perf: { title: '🔥 Performance' },
|
||||||
|
fix: { title: '🩹 Fixes' },
|
||||||
|
refactor: { title: '💅 Refactors' },
|
||||||
|
docs: { title: '📖 Documentation' },
|
||||||
|
build: { title: '📦 Build' },
|
||||||
|
types: { title: '🌊 Types' },
|
||||||
|
chore: { title: '🏡 Chore' },
|
||||||
|
examples: { title: '🏀 Examples' },
|
||||||
|
test: { title: '✅ Tests' },
|
||||||
|
style: { title: '🎨 Styles' },
|
||||||
|
ci: { title: '🤖 CI' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Version Title
|
||||||
|
markdown.push('', `## ${releaseVersion}`, '');
|
||||||
|
|
||||||
|
for (const type of Object.keys(commitTypes)) {
|
||||||
|
const group = typeGroups[type];
|
||||||
|
if (!group || group.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown.push('', '### ' + commitTypes[type].title, '');
|
||||||
|
for (const commit of group.reverse()) {
|
||||||
|
const line = formatCommit(commit, githubRequestConfig);
|
||||||
|
markdown.push(line);
|
||||||
|
if (commit.isBreaking) {
|
||||||
|
breakingChanges.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakingChanges.length > 0) {
|
||||||
|
markdown.push('', '#### ⚠️ Breaking Changes', '', ...breakingChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _authors = new Map<string, { email: Set<string>; github?: string }>();
|
||||||
|
for (const commit of commits) {
|
||||||
|
if (!commit.author) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = formatName(commit.author.name);
|
||||||
|
if (!name || name.includes('[bot]')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (_authors.has(name)) {
|
||||||
|
const entry = _authors.get(name);
|
||||||
|
entry.email.add(commit.author.email);
|
||||||
|
} else {
|
||||||
|
_authors.set(name, { email: new Set([commit.author.email]) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to map authors to github usernames
|
||||||
|
await Promise.all(
|
||||||
|
[..._authors.keys()].map(async (authorName) => {
|
||||||
|
const meta = _authors.get(authorName);
|
||||||
|
for (const email of meta.email) {
|
||||||
|
// For these pseudo-anonymized emails we can just extract the Github username from before the @
|
||||||
|
if (email.endsWith('@users.noreply.github.com')) {
|
||||||
|
meta.github = email.split('@')[0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Look up any other emails against the ungh.cc API
|
||||||
|
const { data } = await axios
|
||||||
|
.get<any, { data?: { user?: { username: string } } }>(
|
||||||
|
`https://ungh.cc/users/find/${email}`
|
||||||
|
)
|
||||||
|
.catch(() => ({ data: { user: null } }));
|
||||||
|
if (data?.user) {
|
||||||
|
meta.github = data.user.username;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const authors = [..._authors.entries()].map((e) => ({ name: e[0], ...e[1] }));
|
||||||
|
|
||||||
|
if (authors.length > 0) {
|
||||||
|
markdown.push(
|
||||||
|
'',
|
||||||
|
'### ' + '❤️ Thank You',
|
||||||
|
'',
|
||||||
|
...authors.map((i) => {
|
||||||
|
const _email = [...i.email].find(
|
||||||
|
(e) => !e.includes('noreply.github.com')
|
||||||
|
);
|
||||||
|
const email = _email ? `<${_email}>` : '';
|
||||||
|
const github = i.github ? `@${i.github}` : '';
|
||||||
|
return `- ${i.name} ${github || email}`;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown.join('\n').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncGithubRelease(
|
||||||
|
githubRequestConfig: GithubRequestConfig,
|
||||||
|
release: { version: string; body: string },
|
||||||
|
existingGithubReleaseForVersion?: GithubRelease
|
||||||
|
) {
|
||||||
|
const ghRelease: GithubRelease = {
|
||||||
|
tag_name: release.version,
|
||||||
|
name: release.version,
|
||||||
|
body: release.body,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newGhRelease = await (existingGithubReleaseForVersion
|
||||||
|
? updateGithubRelease(
|
||||||
|
githubRequestConfig,
|
||||||
|
existingGithubReleaseForVersion.id,
|
||||||
|
ghRelease
|
||||||
|
)
|
||||||
|
: createGithubRelease(githubRequestConfig, ghRelease));
|
||||||
|
return {
|
||||||
|
status: existingGithubReleaseForVersion ? 'updated' : 'created',
|
||||||
|
id: newGhRelease.id,
|
||||||
|
url: newGhRelease.html_url,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'manual',
|
||||||
|
error,
|
||||||
|
url: githubNewReleaseURL(githubRequestConfig, release),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveGithubToken(): Promise<string | null> {
|
||||||
|
// Try and resolve from the environment
|
||||||
|
const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
||||||
|
if (tokenFromEnv) {
|
||||||
|
return tokenFromEnv;
|
||||||
|
}
|
||||||
|
// Try and resolve from gh CLI installation
|
||||||
|
const ghCLIPath = joinPathFragments(
|
||||||
|
process.env.XDG_CONFIG_HOME || joinPathFragments(homedir(), '.config'),
|
||||||
|
'gh',
|
||||||
|
'hosts.yml'
|
||||||
|
);
|
||||||
|
if (existsSync(ghCLIPath)) {
|
||||||
|
const yamlContents = await fsp.readFile(ghCLIPath, 'utf8');
|
||||||
|
const { load } = require('@zkochan/js-yaml');
|
||||||
|
const ghCLIConfig = load(yamlContents);
|
||||||
|
return ghCLIConfig['github.com'].oauth_token;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGithubReleaseByTag(
|
||||||
|
config: GithubRequestConfig,
|
||||||
|
tag: string
|
||||||
|
): Promise<GithubRelease> {
|
||||||
|
return await makeGithubRequest(
|
||||||
|
config,
|
||||||
|
`/repos/${config.repo}/releases/tags/${tag}`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeGithubRequest(
|
||||||
|
config: GithubRequestConfig,
|
||||||
|
url: string,
|
||||||
|
opts: AxiosRequestConfig = {}
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
await axios<any, any>(url, {
|
||||||
|
...opts,
|
||||||
|
baseURL: 'https://api.github.com',
|
||||||
|
headers: {
|
||||||
|
...(opts.headers as any),
|
||||||
|
Authorization: config.token ? `Bearer ${config.token}` : undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createGithubRelease(
|
||||||
|
config: GithubRequestConfig,
|
||||||
|
body: GithubRelease
|
||||||
|
) {
|
||||||
|
return await makeGithubRequest(config, `/repos/${config.repo}/releases`, {
|
||||||
|
method: 'POST',
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGithubRelease(
|
||||||
|
config: GithubRequestConfig,
|
||||||
|
id: string,
|
||||||
|
body: GithubRelease
|
||||||
|
) {
|
||||||
|
return await makeGithubRequest(
|
||||||
|
config,
|
||||||
|
`/repos/${config.repo}/releases/${id}`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
data: body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function githubNewReleaseURL(
|
||||||
|
config: GithubRequestConfig,
|
||||||
|
release: { version: string; body: string }
|
||||||
|
) {
|
||||||
|
return `https://github.com/${config.repo}/releases/new?tag=v${
|
||||||
|
release.version
|
||||||
|
}&title=v${release.version}&body=${encodeURIComponent(release.body)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepoProvider = 'github';
|
||||||
|
|
||||||
|
const providerToRefSpec: Record<
|
||||||
|
RepoProvider,
|
||||||
|
Record<Reference['type'], string>
|
||||||
|
> = {
|
||||||
|
github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatReference(
|
||||||
|
ref: Reference,
|
||||||
|
githubRequestConfig: GithubRequestConfig
|
||||||
|
) {
|
||||||
|
const refSpec = providerToRefSpec['github'];
|
||||||
|
return `[${ref.value}](https://github.com/${githubRequestConfig.repo}/${
|
||||||
|
refSpec[ref.type]
|
||||||
|
}/${ref.value.replace(/^#/, '')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCommit(
|
||||||
|
commit: GitCommit,
|
||||||
|
githubRequestConfig: GithubRequestConfig
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
'- ' +
|
||||||
|
(commit.scope ? `**${commit.scope.trim()}:** ` : '') +
|
||||||
|
(commit.isBreaking ? '⚠️ ' : '') +
|
||||||
|
commit.description +
|
||||||
|
formatReferences(commit.references, githubRequestConfig)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReferences(
|
||||||
|
references: Reference[],
|
||||||
|
githubRequestConfig: GithubRequestConfig
|
||||||
|
) {
|
||||||
|
const pr = references.filter((ref) => ref.type === 'pull-request');
|
||||||
|
const issue = references.filter((ref) => ref.type === 'issue');
|
||||||
|
if (pr.length > 0 || issue.length > 0) {
|
||||||
|
return (
|
||||||
|
' (' +
|
||||||
|
[...pr, ...issue]
|
||||||
|
.map((ref) => formatReference(ref, githubRequestConfig))
|
||||||
|
.join(', ') +
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (references.length > 0) {
|
||||||
|
return ' (' + formatReference(references[0], githubRequestConfig) + ')';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatName(name = '') {
|
||||||
|
return name
|
||||||
|
.split(' ')
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupBy(items: any[], key: string) {
|
||||||
|
const groups = {};
|
||||||
|
for (const item of items) {
|
||||||
|
groups[item[key]] = groups[item[key]] || [];
|
||||||
|
groups[item[key]].push(item);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
43
packages/nx/src/command-line/release/utils/launch-editor.ts
Normal file
43
packages/nx/src/command-line/release/utils/launch-editor.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { execSync, spawn } from 'node:child_process';
|
||||||
|
|
||||||
|
export async function launchEditor(filePath: string) {
|
||||||
|
// Inspired by what git does
|
||||||
|
const editorCommand =
|
||||||
|
process.env.GIT_EDITOR ||
|
||||||
|
getGitConfig('core.editor') ||
|
||||||
|
process.env.VISUAL ||
|
||||||
|
process.env.EDITOR ||
|
||||||
|
'vi';
|
||||||
|
|
||||||
|
const { cmd, args } = parseCommand(editorCommand);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const editorProcess = spawn(cmd, [...args, filePath], {
|
||||||
|
stdio: 'inherit', // This will ensure the editor uses the current terminal
|
||||||
|
});
|
||||||
|
|
||||||
|
editorProcess.on('exit', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(undefined);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Editor process exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGitConfig(key): string | null {
|
||||||
|
try {
|
||||||
|
return execSync(`git config --get ${key}`).toString().trim();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCommand(commandString) {
|
||||||
|
const parts = commandString.split(/\s+/);
|
||||||
|
return {
|
||||||
|
cmd: parts[0],
|
||||||
|
args: parts.slice(1),
|
||||||
|
};
|
||||||
|
}
|
||||||
16
packages/nx/src/command-line/release/utils/print-diff.ts
Normal file
16
packages/nx/src/command-line/release/utils/print-diff.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import * as chalk from 'chalk';
|
||||||
|
import { diff } from 'jest-diff';
|
||||||
|
|
||||||
|
export function printDiff(before: string, after: string) {
|
||||||
|
console.error(
|
||||||
|
diff(before, after, {
|
||||||
|
omitAnnotationLines: true,
|
||||||
|
contextLines: 1,
|
||||||
|
expand: false,
|
||||||
|
aColor: chalk.red,
|
||||||
|
bColor: chalk.green,
|
||||||
|
patchColor: (s) => '',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { relative } from 'node:path';
|
||||||
|
import { joinPathFragments, workspaceRoot } from '../../../devkit-exports';
|
||||||
|
|
||||||
|
export async function resolveNxJsonConfigErrorMessage(
|
||||||
|
propPath: string[]
|
||||||
|
): Promise<string> {
|
||||||
|
const errorLines = await getJsonConfigLinesForErrorMessage(
|
||||||
|
readFileSync(joinPathFragments(workspaceRoot, 'nx.json'), 'utf-8'),
|
||||||
|
propPath
|
||||||
|
);
|
||||||
|
let nxJsonMessage = `The relevant config is defined here: ${relative(
|
||||||
|
process.cwd(),
|
||||||
|
joinPathFragments(workspaceRoot, 'nx.json')
|
||||||
|
)}`;
|
||||||
|
if (errorLines) {
|
||||||
|
nxJsonMessage += `, lines ${errorLines.startLine}-${errorLines.endLine}`;
|
||||||
|
}
|
||||||
|
return nxJsonMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJsonConfigLinesForErrorMessage(
|
||||||
|
rawConfig: string,
|
||||||
|
jsonPath: string[]
|
||||||
|
): Promise<{ startLine: number; endLine: number } | null> {
|
||||||
|
try {
|
||||||
|
const jsonParser = await import('jsonc-parser');
|
||||||
|
const rootNode = jsonParser.parseTree(rawConfig);
|
||||||
|
const node = jsonParser.findNodeAtLocation(rootNode, jsonPath);
|
||||||
|
return computeJsonLineNumbers(rawConfig, node?.offset, node?.length);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeJsonLineNumbers(
|
||||||
|
inputText: string,
|
||||||
|
startOffset: number,
|
||||||
|
characterCount: number
|
||||||
|
) {
|
||||||
|
let lines = inputText.split('\n');
|
||||||
|
let totalChars = 0;
|
||||||
|
let startLine = 0;
|
||||||
|
let endLine = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
totalChars += lines[i].length + 1; // +1 for '\n' character
|
||||||
|
|
||||||
|
if (!startLine && totalChars >= startOffset) {
|
||||||
|
startLine = i + 1; // +1 because arrays are 0-based
|
||||||
|
}
|
||||||
|
if (totalChars >= startOffset + characterCount) {
|
||||||
|
endLine = i + 1; // +1 because arrays are 0-based
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startLine) {
|
||||||
|
throw new Error('Start offset exceeds the text length');
|
||||||
|
}
|
||||||
|
if (!endLine) {
|
||||||
|
throw new Error(
|
||||||
|
'Character count exceeds the text length after start offset'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { startLine, endLine };
|
||||||
|
}
|
||||||
70
packages/nx/src/command-line/release/utils/semver.spec.ts
Normal file
70
packages/nx/src/command-line/release/utils/semver.spec.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { deriveNewSemverVersion } from './semver';
|
||||||
|
|
||||||
|
describe('deriveNewSemverVersion()', () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
currentVersion: '1.0.0',
|
||||||
|
specifier: 'major',
|
||||||
|
},
|
||||||
|
expected: '2.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
currentVersion: '1.0.0',
|
||||||
|
specifier: 'minor',
|
||||||
|
},
|
||||||
|
expected: '1.1.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
currentVersion: '1.0.0',
|
||||||
|
specifier: 'patch',
|
||||||
|
},
|
||||||
|
expected: '1.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
currentVersion: '1.0.0',
|
||||||
|
specifier: '99.9.9', // exact version
|
||||||
|
},
|
||||||
|
expected: '99.9.9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
currentVersion: '1.0.0',
|
||||||
|
specifier: '99.9.9', // exact version
|
||||||
|
},
|
||||||
|
expected: '99.9.9',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((c, i) => {
|
||||||
|
it(`should derive an appropriate semver version, CASE: ${i}`, () => {
|
||||||
|
expect(
|
||||||
|
deriveNewSemverVersion(c.input.currentVersion, c.input.specifier)
|
||||||
|
).toEqual(c.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the current version is not a valid semver version', () => {
|
||||||
|
expect(() =>
|
||||||
|
deriveNewSemverVersion('not-a-valid-semver-version', 'minor')
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid semver version "not-a-valid-semver-version" provided."`
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
deriveNewSemverVersion('major', 'minor')
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid semver version "major" provided."`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the new version specifier is not a valid semver version or semver keyword', () => {
|
||||||
|
expect(() =>
|
||||||
|
deriveNewSemverVersion('1.0.0', 'foo')
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid semver version specifier "foo" provided. Please provide either a valid semver version or a valid semver version keyword."`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
37
packages/nx/src/command-line/release/utils/semver.ts
Normal file
37
packages/nx/src/command-line/release/utils/semver.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { RELEASE_TYPES, ReleaseType, inc, valid } from 'semver';
|
||||||
|
|
||||||
|
export function isRelativeVersionKeyword(val: string): val is ReleaseType {
|
||||||
|
return RELEASE_TYPES.includes(val as ReleaseType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveNewSemverVersion(
|
||||||
|
currentSemverVersion: string,
|
||||||
|
semverSpecifier: string,
|
||||||
|
preid?: string
|
||||||
|
) {
|
||||||
|
if (!valid(currentSemverVersion)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid semver version "${currentSemverVersion}" provided.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let newVersion = semverSpecifier;
|
||||||
|
if (isRelativeVersionKeyword(semverSpecifier)) {
|
||||||
|
// Derive the new version from the current version combined with the new version specifier.
|
||||||
|
const derivedVersion = inc(currentSemverVersion, semverSpecifier, preid);
|
||||||
|
if (!derivedVersion) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to derive new version from current version "${currentSemverVersion}" and version specifier "${semverSpecifier}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
newVersion = derivedVersion;
|
||||||
|
} else {
|
||||||
|
// Ensure the new version specifier is a valid semver version, given it is not a valid semver keyword
|
||||||
|
if (!valid(semverSpecifier)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid semver version specifier "${semverSpecifier}" provided. Please provide either a valid semver version or a valid semver version keyword.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newVersion;
|
||||||
|
}
|
||||||
428
packages/nx/src/command-line/release/version.ts
Normal file
428
packages/nx/src/command-line/release/version.ts
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
import * as chalk from 'chalk';
|
||||||
|
import * as enquirer from 'enquirer';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { relative } from 'node:path';
|
||||||
|
import { RELEASE_TYPES, valid } from 'semver';
|
||||||
|
import { Generator } from '../../config/misc-interfaces';
|
||||||
|
import { readNxJson } from '../../config/nx-json';
|
||||||
|
import {
|
||||||
|
ProjectGraph,
|
||||||
|
ProjectGraphProjectNode,
|
||||||
|
} from '../../config/project-graph';
|
||||||
|
import {
|
||||||
|
NxJsonConfiguration,
|
||||||
|
joinPathFragments,
|
||||||
|
logger,
|
||||||
|
output,
|
||||||
|
workspaceRoot,
|
||||||
|
} from '../../devkit-exports';
|
||||||
|
import { FsTree, Tree, flushChanges } from '../../generators/tree';
|
||||||
|
import {
|
||||||
|
createProjectGraphAsync,
|
||||||
|
readProjectsConfigurationFromProjectGraph,
|
||||||
|
} from '../../project-graph/project-graph';
|
||||||
|
import { findMatchingProjects } from '../../utils/find-matching-projects';
|
||||||
|
import { combineOptionsForGenerator } from '../../utils/params';
|
||||||
|
import { parseGeneratorString } from '../generate/generate';
|
||||||
|
import { getGeneratorInformation } from '../generate/generator-utils';
|
||||||
|
import { VersionOptions } from './command-object';
|
||||||
|
import { createNxReleaseConfig } from './config/config';
|
||||||
|
import {
|
||||||
|
CATCH_ALL_RELEASE_GROUP,
|
||||||
|
ReleaseGroup,
|
||||||
|
createReleaseGroups,
|
||||||
|
handleCreateReleaseGroupsError,
|
||||||
|
} from './config/create-release-groups';
|
||||||
|
import { printDiff } from './utils/print-diff';
|
||||||
|
import { isRelativeVersionKeyword } from './utils/semver';
|
||||||
|
|
||||||
|
// Reexport for use in plugin release-version generator implementations
|
||||||
|
export { deriveNewSemverVersion } from './utils/semver';
|
||||||
|
|
||||||
|
export interface ReleaseVersionGeneratorSchema {
|
||||||
|
// The projects being versioned in the current execution
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
projectGraph: ProjectGraph;
|
||||||
|
specifier: string;
|
||||||
|
preid?: string;
|
||||||
|
packageRoot?: string;
|
||||||
|
currentVersionResolver?: 'registry' | 'disk';
|
||||||
|
currentVersionResolverMetadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function versionHandler(args: VersionOptions): Promise<void> {
|
||||||
|
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
||||||
|
const nxJson = readNxJson();
|
||||||
|
|
||||||
|
if (args.verbose) {
|
||||||
|
process.env.NX_VERBOSE_LOGGING = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply default configuration to any optional user configuration
|
||||||
|
const nxReleaseConfig = createNxReleaseConfig(nxJson.release);
|
||||||
|
const releaseGroupsData = await createReleaseGroups(
|
||||||
|
projectGraph,
|
||||||
|
nxReleaseConfig.groups
|
||||||
|
);
|
||||||
|
if (releaseGroupsData.error) {
|
||||||
|
return await handleCreateReleaseGroupsError(releaseGroupsData.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = new FsTree(workspaceRoot, args.verbose);
|
||||||
|
|
||||||
|
let { releaseGroups } = releaseGroupsData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User is filtering to a subset of projects. We need to make sure that what they have provided can be reconciled
|
||||||
|
* against their configuration in terms of release groups and the ungroupedProjectsHandling option.
|
||||||
|
*/
|
||||||
|
if (args.projects?.length) {
|
||||||
|
const matchingProjectsForFilter = findMatchingProjects(
|
||||||
|
args.projects,
|
||||||
|
projectGraph.nodes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingProjectsForFilter.length) {
|
||||||
|
output.error({
|
||||||
|
title: `Your --projects filter "${args.projects}" did not match any projects in the workspace`,
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredProjectToReleaseGroup = new Map<string, ReleaseGroup>();
|
||||||
|
const releaseGroupToFilteredProjects = new Map<ReleaseGroup, Set<string>>();
|
||||||
|
|
||||||
|
// Figure out which release groups, if any, that the filtered projects belong to so that we can resolve other config
|
||||||
|
for (const releaseGroup of releaseGroups) {
|
||||||
|
const matchingProjectsForReleaseGroup = findMatchingProjects(
|
||||||
|
releaseGroup.projects,
|
||||||
|
projectGraph.nodes
|
||||||
|
);
|
||||||
|
for (const matchingProject of matchingProjectsForFilter) {
|
||||||
|
if (matchingProjectsForReleaseGroup.includes(matchingProject)) {
|
||||||
|
filteredProjectToReleaseGroup.set(matchingProject, releaseGroup);
|
||||||
|
if (!releaseGroupToFilteredProjects.has(releaseGroup)) {
|
||||||
|
releaseGroupToFilteredProjects.set(releaseGroup, new Set());
|
||||||
|
}
|
||||||
|
releaseGroupToFilteredProjects.get(releaseGroup).add(matchingProject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there are release groups specified, each filtered project must match at least one release
|
||||||
|
* group, otherwise the command + config combination is invalid.
|
||||||
|
*/
|
||||||
|
if (Object.keys(nxReleaseConfig.groups).length) {
|
||||||
|
const unmatchedProjects = matchingProjectsForFilter.filter(
|
||||||
|
(p) => !filteredProjectToReleaseGroup.has(p)
|
||||||
|
);
|
||||||
|
if (unmatchedProjects.length) {
|
||||||
|
output.error({
|
||||||
|
title: `The following projects which match your projects filter "${args.projects}" did not match any configured release groups:`,
|
||||||
|
bodyLines: unmatchedProjects.map((p) => `- ${p}`),
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.note({
|
||||||
|
title: `Your filter "${args.projects}" matched the following projects:`,
|
||||||
|
bodyLines: matchingProjectsForFilter.map((p) => {
|
||||||
|
const releaseGroupForProject = filteredProjectToReleaseGroup.get(p);
|
||||||
|
if (releaseGroupForProject.name === CATCH_ALL_RELEASE_GROUP) {
|
||||||
|
return `- ${p}`;
|
||||||
|
}
|
||||||
|
return `- ${p} (release group "${releaseGroupForProject.name}")`;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter the releaseGroups collection appropriately
|
||||||
|
releaseGroups = releaseGroups.filter((rg) =>
|
||||||
|
releaseGroupToFilteredProjects.has(rg)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run semver versioning for all remaining release groups and filtered projects within them
|
||||||
|
*/
|
||||||
|
for (const releaseGroup of releaseGroups) {
|
||||||
|
const releaseGroupName = releaseGroup.name;
|
||||||
|
|
||||||
|
// Resolve the generator data for the current release group
|
||||||
|
const generatorData = resolveGeneratorData({
|
||||||
|
...extractGeneratorCollectionAndName(
|
||||||
|
`release-group "${releaseGroupName}"`,
|
||||||
|
releaseGroup.version.generator
|
||||||
|
),
|
||||||
|
configGeneratorOptions: releaseGroup.version.generatorOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const semverSpecifier = await resolveSemverSpecifier(
|
||||||
|
args.specifier,
|
||||||
|
`What kind of change is this for the ${
|
||||||
|
releaseGroupToFilteredProjects.get(releaseGroup).size
|
||||||
|
} matched project(s) within release group "${releaseGroupName}"?`,
|
||||||
|
`What is the exact version for the ${
|
||||||
|
releaseGroupToFilteredProjects.get(releaseGroup).size
|
||||||
|
} matched project(s) within release group "${releaseGroupName}"?`
|
||||||
|
);
|
||||||
|
|
||||||
|
await runVersionOnProjects(
|
||||||
|
projectGraph,
|
||||||
|
nxJson,
|
||||||
|
args,
|
||||||
|
tree,
|
||||||
|
generatorData,
|
||||||
|
Array.from(releaseGroupToFilteredProjects.get(releaseGroup)),
|
||||||
|
semverSpecifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
printChanges(tree, !!args.dryRun);
|
||||||
|
|
||||||
|
return process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user is filtering by release group
|
||||||
|
*/
|
||||||
|
if (args.groups?.length) {
|
||||||
|
releaseGroups = releaseGroups.filter((g) => args.groups?.includes(g.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be an impossible state, as we should have explicitly handled any errors/invalid config by now
|
||||||
|
if (!releaseGroups.length) {
|
||||||
|
output.error({
|
||||||
|
title: `No projects could be matched for versioning, please report this case and include your nx.json config`,
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run semver versioning for all remaining release groups
|
||||||
|
*/
|
||||||
|
for (const releaseGroup of releaseGroups) {
|
||||||
|
const releaseGroupName = releaseGroup.name;
|
||||||
|
|
||||||
|
// Resolve the generator data for the current release group
|
||||||
|
const generatorData = resolveGeneratorData({
|
||||||
|
...extractGeneratorCollectionAndName(
|
||||||
|
`release-group "${releaseGroupName}"`,
|
||||||
|
releaseGroup.version.generator
|
||||||
|
),
|
||||||
|
configGeneratorOptions: releaseGroup.version.generatorOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const semverSpecifier = await resolveSemverSpecifier(
|
||||||
|
args.specifier,
|
||||||
|
releaseGroupName === CATCH_ALL_RELEASE_GROUP
|
||||||
|
? `What kind of change is this for all packages?`
|
||||||
|
: `What kind of change is this for release group "${releaseGroupName}"?`,
|
||||||
|
releaseGroupName === CATCH_ALL_RELEASE_GROUP
|
||||||
|
? `What is the exact version for all packages?`
|
||||||
|
: `What is the exact version for release group "${releaseGroupName}"?`
|
||||||
|
);
|
||||||
|
|
||||||
|
await runVersionOnProjects(
|
||||||
|
projectGraph,
|
||||||
|
nxJson,
|
||||||
|
args,
|
||||||
|
tree,
|
||||||
|
generatorData,
|
||||||
|
releaseGroup.projects,
|
||||||
|
semverSpecifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
printChanges(tree, !!args.dryRun);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runVersionOnProjects(
|
||||||
|
projectGraph: ProjectGraph,
|
||||||
|
nxJson: NxJsonConfiguration,
|
||||||
|
args: VersionOptions,
|
||||||
|
tree: Tree,
|
||||||
|
generatorData: GeneratorData,
|
||||||
|
projectNames: string[],
|
||||||
|
newVersionSpecifier: string
|
||||||
|
) {
|
||||||
|
// Should be impossible state
|
||||||
|
if (!newVersionSpecifier) {
|
||||||
|
output.error({
|
||||||
|
title: `No version or semver keyword could be determined`,
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
// Specifier could be user provided so we need to validate it
|
||||||
|
if (
|
||||||
|
!valid(newVersionSpecifier) &&
|
||||||
|
!isRelativeVersionKeyword(newVersionSpecifier)
|
||||||
|
) {
|
||||||
|
output.error({
|
||||||
|
title: `The given version specifier "${newVersionSpecifier}" is not valid. You provide an exact version or a valid semver keyword such as "major", "minor", "patch", etc.`,
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatorOptions: ReleaseVersionGeneratorSchema = {
|
||||||
|
projects: projectNames.map((p) => projectGraph.nodes[p]),
|
||||||
|
projectGraph,
|
||||||
|
specifier: newVersionSpecifier,
|
||||||
|
preid: args.preid,
|
||||||
|
...generatorData.configGeneratorOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply generator defaults from schema.json file etc
|
||||||
|
const combinedOpts = await combineOptionsForGenerator(
|
||||||
|
generatorOptions as any,
|
||||||
|
generatorData.collectionName,
|
||||||
|
generatorData.normalizedGeneratorName,
|
||||||
|
readProjectsConfigurationFromProjectGraph(projectGraph),
|
||||||
|
nxJson,
|
||||||
|
generatorData.schema,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
relative(process.cwd(), workspaceRoot),
|
||||||
|
args.verbose
|
||||||
|
);
|
||||||
|
|
||||||
|
const releaseVersionGenerator = generatorData.implementationFactory();
|
||||||
|
await releaseVersionGenerator(tree, combinedOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printChanges(tree: Tree, isDryRun: boolean) {
|
||||||
|
const changes = tree.listChanges();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Print the changes
|
||||||
|
changes.forEach((f) => {
|
||||||
|
if (f.type === 'CREATE') {
|
||||||
|
console.error(
|
||||||
|
`${chalk.green('CREATE')} ${f.path}${
|
||||||
|
isDryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
printDiff('', f.content?.toString() || '');
|
||||||
|
} else if (f.type === 'UPDATE') {
|
||||||
|
console.error(
|
||||||
|
`${chalk.white('UPDATE')} ${f.path}${
|
||||||
|
isDryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
const currentContentsOnDisk = readFileSync(
|
||||||
|
joinPathFragments(tree.root, f.path)
|
||||||
|
).toString();
|
||||||
|
printDiff(currentContentsOnDisk, f.content?.toString() || '');
|
||||||
|
} else if (f.type === 'DELETE') {
|
||||||
|
throw new Error(
|
||||||
|
'Unexpected DELETE change, please report this as an issue'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDryRun) {
|
||||||
|
flushChanges(workspaceRoot, changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDryRun) {
|
||||||
|
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveSemverSpecifier(
|
||||||
|
cliArgSpecifier: string,
|
||||||
|
selectionMessage: string,
|
||||||
|
customVersionMessage: string
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
let newVersionSpecifier = cliArgSpecifier;
|
||||||
|
// If the user didn't provide a new version specifier directly on the CLI, prompt for one
|
||||||
|
if (!newVersionSpecifier) {
|
||||||
|
const reply = await enquirer.prompt<{ specifier: string }>([
|
||||||
|
{
|
||||||
|
name: 'specifier',
|
||||||
|
message: selectionMessage,
|
||||||
|
type: 'select',
|
||||||
|
choices: [
|
||||||
|
...RELEASE_TYPES.map((t) => ({ name: t, message: t })),
|
||||||
|
{
|
||||||
|
name: 'custom',
|
||||||
|
message: 'Custom exact version',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
if (reply.specifier !== 'custom') {
|
||||||
|
newVersionSpecifier = reply.specifier;
|
||||||
|
} else {
|
||||||
|
const reply = await enquirer.prompt<{ specifier: string }>([
|
||||||
|
{
|
||||||
|
name: 'specifier',
|
||||||
|
message: customVersionMessage,
|
||||||
|
type: 'input',
|
||||||
|
validate: (input) => {
|
||||||
|
if (valid(input)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return 'Please enter a valid semver version';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
newVersionSpecifier = reply.specifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newVersionSpecifier;
|
||||||
|
} catch {
|
||||||
|
// We need to catch the error from enquirer prompt, otherwise yargs will print its help
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractGeneratorCollectionAndName(
|
||||||
|
description: string,
|
||||||
|
generatorString: string
|
||||||
|
) {
|
||||||
|
let collectionName: string;
|
||||||
|
let generatorName: string;
|
||||||
|
const parsedGeneratorString = parseGeneratorString(generatorString);
|
||||||
|
collectionName = parsedGeneratorString.collection;
|
||||||
|
generatorName = parsedGeneratorString.generator;
|
||||||
|
|
||||||
|
if (!collectionName || !generatorName) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid generator string: ${generatorString} used for ${description}. Must be in the format of [collectionName]:[generatorName]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { collectionName, generatorName };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneratorData {
|
||||||
|
collectionName: string;
|
||||||
|
generatorName: string;
|
||||||
|
configGeneratorOptions: NxJsonConfiguration['release']['groups'][number]['version']['generatorOptions'];
|
||||||
|
normalizedGeneratorName: string;
|
||||||
|
schema: any;
|
||||||
|
implementationFactory: () => Generator<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveGeneratorData({
|
||||||
|
collectionName,
|
||||||
|
generatorName,
|
||||||
|
configGeneratorOptions,
|
||||||
|
}): GeneratorData {
|
||||||
|
const { normalizedGeneratorName, schema, implementationFactory } =
|
||||||
|
getGeneratorInformation(collectionName, generatorName, workspaceRoot);
|
||||||
|
|
||||||
|
return {
|
||||||
|
collectionName,
|
||||||
|
generatorName,
|
||||||
|
configGeneratorOptions,
|
||||||
|
normalizedGeneratorName,
|
||||||
|
schema,
|
||||||
|
implementationFactory,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -65,7 +65,7 @@ export async function runMany(
|
|||||||
projectNames
|
projectNames
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await runCommand(
|
const status = await runCommand(
|
||||||
projects,
|
projects,
|
||||||
projectGraph,
|
projectGraph,
|
||||||
{ nxJson },
|
{ nxJson },
|
||||||
@ -75,6 +75,9 @@ export async function runMany(
|
|||||||
extraTargetDependencies,
|
extraTargetDependencies,
|
||||||
extraOptions
|
extraOptions
|
||||||
);
|
);
|
||||||
|
// fix for https://github.com/nrwl/nx/issues/1666
|
||||||
|
if (process.stdin['unref']) (process.stdin as any).unref();
|
||||||
|
process.exit(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export async function runOne(
|
|||||||
projectNames
|
projectNames
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await runCommand(
|
const status = await runCommand(
|
||||||
projects,
|
projects,
|
||||||
projectGraph,
|
projectGraph,
|
||||||
{ nxJson },
|
{ nxJson },
|
||||||
@ -89,6 +89,9 @@ export async function runOne(
|
|||||||
extraTargetDependencies,
|
extraTargetDependencies,
|
||||||
extraOptions
|
extraOptions
|
||||||
);
|
);
|
||||||
|
// fix for https://github.com/nrwl/nx/issues/1666
|
||||||
|
if (process.stdin['unref']) (process.stdin as any).unref();
|
||||||
|
process.exit(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,33 @@
|
|||||||
import { Argv } from 'yargs';
|
import { Argv } from 'yargs';
|
||||||
|
|
||||||
export function withExcludeOption(yargs: Argv) {
|
interface ExcludeOptions {
|
||||||
|
exclude: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withExcludeOption(yargs: Argv): Argv<ExcludeOptions> {
|
||||||
return yargs.option('exclude', {
|
return yargs.option('exclude', {
|
||||||
describe: 'Exclude certain projects from being processed',
|
describe: 'Exclude certain projects from being processed',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
coerce: parseCSV,
|
coerce: parseCSV,
|
||||||
});
|
}) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withRunOptions(yargs: Argv) {
|
export interface RunOptions {
|
||||||
|
exclude: string;
|
||||||
|
parallel: string;
|
||||||
|
maxParallel: number;
|
||||||
|
runner: string;
|
||||||
|
prod: boolean;
|
||||||
|
graph: string;
|
||||||
|
verbose: boolean;
|
||||||
|
nxBail: boolean;
|
||||||
|
nxIgnoreCycles: boolean;
|
||||||
|
skipNxCache: boolean;
|
||||||
|
cloud: boolean;
|
||||||
|
dte: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withRunOptions<T>(yargs: Argv<T>): Argv<T & RunOptions> {
|
||||||
return withExcludeOption(yargs)
|
return withExcludeOption(yargs)
|
||||||
.option('parallel', {
|
.option('parallel', {
|
||||||
describe: 'Max number of parallel processes [default is 3]',
|
describe: 'Max number of parallel processes [default is 3]',
|
||||||
@ -47,17 +66,17 @@ export function withRunOptions(yargs: Argv) {
|
|||||||
describe:
|
describe:
|
||||||
'Prints additional information about the commands (e.g., stack traces)',
|
'Prints additional information about the commands (e.g., stack traces)',
|
||||||
})
|
})
|
||||||
.option('nx-bail', {
|
.option('nxBail', {
|
||||||
describe: 'Stop command execution after the first failed task',
|
describe: 'Stop command execution after the first failed task',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
.option('nx-ignore-cycles', {
|
.option('nxIgnoreCycles', {
|
||||||
describe: 'Ignore cycles in the task graph',
|
describe: 'Ignore cycles in the task graph',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
.options('skip-nx-cache', {
|
.options('skipNxCache', {
|
||||||
describe:
|
describe:
|
||||||
'Rerun the tasks even when the results are available in the cache',
|
'Rerun the tasks even when the results are available in the cache',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -70,7 +89,7 @@ export function withRunOptions(yargs: Argv) {
|
|||||||
.options('dte', {
|
.options('dte', {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
});
|
}) as Argv<Omit<RunOptions, 'projects' | 'exclude'>> as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withTargetAndConfigurationOption(
|
export function withTargetAndConfigurationOption(
|
||||||
@ -146,7 +165,17 @@ export function withAffectedOptions(yargs: Argv) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withRunManyOptions(yargs: Argv) {
|
export interface RunManyOptions extends RunOptions {
|
||||||
|
projects: string[];
|
||||||
|
/**
|
||||||
|
* @deprecated This is deprecated
|
||||||
|
*/
|
||||||
|
all: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withRunManyOptions<T>(
|
||||||
|
yargs: Argv<T>
|
||||||
|
): Argv<T & RunManyOptions> {
|
||||||
return withRunOptions(yargs)
|
return withRunOptions(yargs)
|
||||||
.parserConfiguration({
|
.parserConfiguration({
|
||||||
'strip-dashed': true,
|
'strip-dashed': true,
|
||||||
@ -165,16 +194,22 @@ export function withRunManyOptions(yargs: Argv) {
|
|||||||
'[deprecated] `run-many` runs all targets on all projects in the workspace if no projects are provided. This option is no longer required.',
|
'[deprecated] `run-many` runs all targets on all projects in the workspace if no projects are provided. This option is no longer required.',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
});
|
}) as Argv<T & RunManyOptions>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withOverrides(args: any): any {
|
export function withOverrides<T extends { _: Array<string | number> }>(
|
||||||
args.__overrides_unparsed__ = (args['--'] ?? args._.slice(1)).map((v) =>
|
args: T,
|
||||||
v.toString()
|
commandLevel: number = 1
|
||||||
|
): T & { __overrides_unparsed__: string[] } {
|
||||||
|
const unparsedArgs: string[] = (args['--'] ?? args._.slice(commandLevel)).map(
|
||||||
|
(v) => v.toString()
|
||||||
);
|
);
|
||||||
delete args['--'];
|
delete args['--'];
|
||||||
delete args._;
|
delete args._;
|
||||||
return args;
|
return {
|
||||||
|
...args,
|
||||||
|
__overrides_unparsed__: unparsedArgs,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withOutputStyleOption(
|
export function withOutputStyleOption(
|
||||||
@ -279,9 +314,9 @@ export function withRunOneOptions(yargs: Argv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCSV(args: string[] | string) {
|
export function parseCSV(args: string[] | string): string[] {
|
||||||
if (!args) {
|
if (!args) {
|
||||||
return args;
|
return [];
|
||||||
}
|
}
|
||||||
if (Array.isArray(args)) {
|
if (Array.isArray(args)) {
|
||||||
return args;
|
return args;
|
||||||
|
|||||||
@ -50,6 +50,35 @@ interface NxInstallationConfiguration {
|
|||||||
plugins?: Record<string, string>;
|
plugins?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **ALPHA**
|
||||||
|
*/
|
||||||
|
interface NxReleaseVersionConfiguration {
|
||||||
|
generator: string;
|
||||||
|
generatorOptions?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **ALPHA**
|
||||||
|
*/
|
||||||
|
interface NxReleaseConfiguration {
|
||||||
|
/**
|
||||||
|
* @note: When no groups are configured at all (the default), all projects in the workspace are treated as
|
||||||
|
* if they were in a release group together.
|
||||||
|
*/
|
||||||
|
groups?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
projects: string[] | string;
|
||||||
|
/**
|
||||||
|
* If no version config is provided for the group, we will assume that @nx/js:release-version
|
||||||
|
* is the desired generator implementation, allowing for terser config for the common case.
|
||||||
|
*/
|
||||||
|
version?: NxReleaseVersionConfiguration;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nx.json configuration
|
* Nx.json configuration
|
||||||
*
|
*
|
||||||
@ -158,6 +187,11 @@ export interface NxJsonConfiguration<T = '*' | string[]> {
|
|||||||
* useful for workspaces that don't have a root package.json + node_modules.
|
* useful for workspaces that don't have a root package.json + node_modules.
|
||||||
*/
|
*/
|
||||||
installation?: NxInstallationConfiguration;
|
installation?: NxInstallationConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **ALPHA**: Configuration for `nx release` (versioning and publishing of applications and libraries)
|
||||||
|
*/
|
||||||
|
release?: NxReleaseConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readNxJson(root: string = workspaceRoot): NxJsonConfiguration {
|
export function readNxJson(root: string = workspaceRoot): NxJsonConfiguration {
|
||||||
|
|||||||
@ -9,7 +9,13 @@ const libConfig = (root, name?: string) => ({
|
|||||||
projectType: 'library',
|
projectType: 'library',
|
||||||
root: `libs/${root}`,
|
root: `libs/${root}`,
|
||||||
sourceRoot: `libs/${root}/src`,
|
sourceRoot: `libs/${root}/src`,
|
||||||
targets: {},
|
targets: {
|
||||||
|
'nx-release-publish': {
|
||||||
|
dependsOn: ['^nx-release-publish'],
|
||||||
|
executor: '@nx/js:release-publish',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const packageLibConfig = (root, name?: string) => ({
|
const packageLibConfig = (root, name?: string) => ({
|
||||||
@ -17,7 +23,13 @@ const packageLibConfig = (root, name?: string) => ({
|
|||||||
root,
|
root,
|
||||||
sourceRoot: root,
|
sourceRoot: root,
|
||||||
projectType: 'library',
|
projectType: 'library',
|
||||||
targets: {},
|
targets: {
|
||||||
|
'nx-release-publish': {
|
||||||
|
dependsOn: ['^nx-release-publish'],
|
||||||
|
executor: '@nx/js:release-publish',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Workspaces', () => {
|
describe('Workspaces', () => {
|
||||||
@ -86,7 +98,13 @@ describe('Workspaces', () => {
|
|||||||
root: 'libs/lib1',
|
root: 'libs/lib1',
|
||||||
sourceRoot: 'libs/lib1/src',
|
sourceRoot: 'libs/lib1/src',
|
||||||
projectType: 'library',
|
projectType: 'library',
|
||||||
targets: {},
|
targets: {
|
||||||
|
'nx-release-publish': {
|
||||||
|
dependsOn: ['^nx-release-publish'],
|
||||||
|
executor: '@nx/js:release-publish',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(projects.lib2).toEqual(lib2Config);
|
expect(projects.lib2).toEqual(lib2Config);
|
||||||
expect(projects['domain-lib3']).toEqual(domainPackageConfig);
|
expect(projects['domain-lib3']).toEqual(domainPackageConfig);
|
||||||
@ -127,7 +145,13 @@ describe('Workspaces', () => {
|
|||||||
root: 'packages/my-package',
|
root: 'packages/my-package',
|
||||||
sourceRoot: 'packages/my-package',
|
sourceRoot: 'packages/my-package',
|
||||||
projectType: 'library',
|
projectType: 'library',
|
||||||
targets: {},
|
targets: {
|
||||||
|
'nx-release-publish': {
|
||||||
|
dependsOn: ['^nx-release-publish'],
|
||||||
|
executor: '@nx/js:release-publish',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export function updateWorkspaceConfiguration(
|
|||||||
affected,
|
affected,
|
||||||
extends: ext,
|
extends: ext,
|
||||||
installation,
|
installation,
|
||||||
|
release,
|
||||||
} = workspaceConfig;
|
} = workspaceConfig;
|
||||||
|
|
||||||
const nxJson: Required<NxJsonConfiguration> = {
|
const nxJson: Required<NxJsonConfiguration> = {
|
||||||
@ -56,6 +57,7 @@ export function updateWorkspaceConfiguration(
|
|||||||
defaultProject,
|
defaultProject,
|
||||||
extends: ext,
|
extends: ext,
|
||||||
installation,
|
installation,
|
||||||
|
release,
|
||||||
};
|
};
|
||||||
|
|
||||||
updateNxJson(tree, nxJson);
|
updateNxJson(tree, nxJson);
|
||||||
|
|||||||
@ -11,6 +11,14 @@ import {
|
|||||||
import { CreateNodesContext } from '../../../utils/nx-plugin';
|
import { CreateNodesContext } from '../../../utils/nx-plugin';
|
||||||
const { createNodes } = CreateProjectJsonProjectsPlugin;
|
const { createNodes } = CreateProjectJsonProjectsPlugin;
|
||||||
|
|
||||||
|
const defaultReleasePublishTarget = {
|
||||||
|
'nx-release-publish': {
|
||||||
|
dependsOn: ['^nx-release-publish'],
|
||||||
|
executor: '@nx/js:release-publish',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('nx project.json plugin', () => {
|
describe('nx project.json plugin', () => {
|
||||||
let context: CreateNodesContext;
|
let context: CreateNodesContext;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -72,6 +80,13 @@ describe('nx project.json plugin', () => {
|
|||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"options": {},
|
"options": {},
|
||||||
},
|
},
|
||||||
|
"nx-release-publish": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^nx-release-publish",
|
||||||
|
],
|
||||||
|
"executor": "@nx/js:release-publish",
|
||||||
|
"options": {},
|
||||||
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"executor": "nx:run-script",
|
"executor": "nx:run-script",
|
||||||
"options": {
|
"options": {
|
||||||
@ -115,7 +130,10 @@ describe('nx project.json plugin', () => {
|
|||||||
packageJson,
|
packageJson,
|
||||||
projectJsonTargets
|
projectJsonTargets
|
||||||
);
|
);
|
||||||
expect(result).toEqual(projectJsonTargets);
|
expect(result).toEqual({
|
||||||
|
...projectJsonTargets,
|
||||||
|
...defaultReleasePublishTarget,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide targets from project.json and package.json', () => {
|
it('should provide targets from project.json and package.json', () => {
|
||||||
@ -135,6 +153,7 @@ describe('nx project.json plugin', () => {
|
|||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
...projectJsonTargets,
|
...projectJsonTargets,
|
||||||
build: packageJsonBuildTarget,
|
build: packageJsonBuildTarget,
|
||||||
|
...defaultReleasePublishTarget,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -158,6 +177,7 @@ describe('nx project.json plugin', () => {
|
|||||||
);
|
);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
build: { ...packageJsonBuildTarget, outputs: ['custom'] },
|
build: { ...packageJsonBuildTarget, outputs: ['custom'] },
|
||||||
|
...defaultReleasePublishTarget,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -171,6 +191,7 @@ describe('nx project.json plugin', () => {
|
|||||||
script: 'build',
|
script: 'build',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...defaultReleasePublishTarget,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -200,6 +221,7 @@ describe('nx project.json plugin', () => {
|
|||||||
executor: 'nx:run-script',
|
executor: 'nx:run-script',
|
||||||
options: { script: 'test' },
|
options: { script: 'test' },
|
||||||
},
|
},
|
||||||
|
...defaultReleasePublishTarget,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -233,6 +255,7 @@ describe('nx project.json plugin', () => {
|
|||||||
executor: 'nx:run-script',
|
executor: 'nx:run-script',
|
||||||
options: { script: 'test' },
|
options: { script: 'test' },
|
||||||
},
|
},
|
||||||
|
...defaultReleasePublishTarget,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -144,7 +144,7 @@ export async function runCommand(
|
|||||||
initiatingProject: string | null,
|
initiatingProject: string | null,
|
||||||
extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>,
|
extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>,
|
||||||
extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean }
|
extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean }
|
||||||
) {
|
): Promise<NodeJS.Process['exitCode']> {
|
||||||
const status = await handleErrors(
|
const status = await handleErrors(
|
||||||
process.env.NX_VERBOSE_LOGGING === 'true',
|
process.env.NX_VERBOSE_LOGGING === 'true',
|
||||||
async () => {
|
async () => {
|
||||||
@ -189,9 +189,8 @@ export async function runCommand(
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// fix for https://github.com/nrwl/nx/issues/1666
|
|
||||||
if (process.stdin['unref']) (process.stdin as any).unref();
|
return status;
|
||||||
process.exit(status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEnvVarsBasedOnArgs(nxArgs: NxArgs, loadDotEnvFiles: boolean) {
|
function setEnvVarsBasedOnArgs(nxArgs: NxArgs, loadDotEnvFiles: boolean) {
|
||||||
|
|||||||
@ -37,6 +37,23 @@ export interface NxArgs {
|
|||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createOverrides(__overrides_unparsed__: string[] = []) {
|
||||||
|
let overrides =
|
||||||
|
yargsParser(__overrides_unparsed__, {
|
||||||
|
configuration: {
|
||||||
|
'camel-case-expansion': false,
|
||||||
|
'dot-notation': true,
|
||||||
|
},
|
||||||
|
}) || {};
|
||||||
|
|
||||||
|
if (!overrides._ || overrides._.length === 0) {
|
||||||
|
delete overrides._;
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides.__overrides_unparsed__ = __overrides_unparsed__;
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
export function splitArgsIntoNxArgsAndOverrides(
|
export function splitArgsIntoNxArgsAndOverrides(
|
||||||
args: { [k: string]: any },
|
args: { [k: string]: any },
|
||||||
mode: 'run-one' | 'run-many' | 'affected' | 'print-affected',
|
mode: 'run-one' | 'run-many' | 'affected' | 'print-affected',
|
||||||
@ -66,18 +83,8 @@ export function splitArgsIntoNxArgsAndOverrides(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nxArgs: RawNxArgs = args;
|
const nxArgs: RawNxArgs = args;
|
||||||
let overrides = yargsParser(args.__overrides_unparsed__ as string[], {
|
|
||||||
configuration: {
|
|
||||||
'camel-case-expansion': false,
|
|
||||||
'dot-notation': true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!overrides._ || overrides._.length === 0) {
|
let overrides = createOverrides(args.__overrides_unparsed__);
|
||||||
delete overrides._;
|
|
||||||
}
|
|
||||||
|
|
||||||
overrides.__overrides_unparsed__ = args.__overrides_unparsed__;
|
|
||||||
delete (nxArgs as any).$0;
|
delete (nxArgs as any).$0;
|
||||||
delete (nxArgs as any).__overrides_unparsed__;
|
delete (nxArgs as any).__overrides_unparsed__;
|
||||||
|
|
||||||
@ -322,7 +329,7 @@ export function getProjectRoots(
|
|||||||
return projectNames.map((name) => nodes[name].data.root);
|
return projectNames.map((name) => nodes[name].data.root);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readGraphFileFromGraphArg({ graph }: NxArgs) {
|
export function readGraphFileFromGraphArg({ graph }: Pick<NxArgs, 'graph'>) {
|
||||||
return typeof graph === 'string' && graph !== 'true' && graph !== ''
|
return typeof graph === 'string' && graph !== 'true' && graph !== ''
|
||||||
? graph
|
? graph
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@ -70,6 +70,11 @@ describe('readTargetsFromPackageJson', () => {
|
|||||||
script: 'build',
|
script: 'build',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'nx-release-publish': {
|
||||||
|
dependsOn: ['^nx-release-publish'],
|
||||||
|
executor: '@nx/js:release-publish',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -90,6 +95,11 @@ describe('readTargetsFromPackageJson', () => {
|
|||||||
});
|
});
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
build: { ...packageJsonBuildTarget, outputs: ['custom'] },
|
build: { ...packageJsonBuildTarget, outputs: ['custom'] },
|
||||||
|
'nx-release-publish': {
|
||||||
|
dependsOn: ['^nx-release-publish'],
|
||||||
|
executor: '@nx/js:release-publish',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,6 +121,11 @@ describe('readTargetsFromPackageJson', () => {
|
|||||||
executor: 'nx:run-script',
|
executor: 'nx:run-script',
|
||||||
options: { script: 'test' },
|
options: { script: 'test' },
|
||||||
},
|
},
|
||||||
|
'nx-release-publish': {
|
||||||
|
dependsOn: ['^nx-release-publish'],
|
||||||
|
executor: '@nx/js:release-publish',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -141,6 +141,16 @@ export function readTargetsFromPackageJson({ scripts, nx }: PackageJson) {
|
|||||||
res[script] = buildTargetFromScript(script, nx);
|
res[script] = buildTargetFromScript(script, nx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add implicit nx-release-publish target for all package.json files to allow for lightweight configuration for package based repos
|
||||||
|
if (!res['nx-release-publish']) {
|
||||||
|
res['nx-release-publish'] = {
|
||||||
|
dependsOn: ['^nx-release-publish'],
|
||||||
|
executor: '@nx/js:release-publish',
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1770
pnpm-lock.yaml
generated
1770
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user