fix(release): respect root .npmrc registry settings for publishing

This commit is contained in:
Austin Fahsl 2024-04-02 13:53:14 -06:00 committed by GitHub
parent 902da5db58
commit 12afa20210
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1162 additions and 102 deletions

View File

@ -2300,6 +2300,14 @@
"children": [], "children": [],
"disableCollapsible": false "disableCollapsible": false
}, },
{
"name": "Configure Custom Registries",
"path": "/recipes/nx-release/configure-custom-registries",
"id": "configure-custom-registries",
"isExternal": false,
"children": [],
"disableCollapsible": false
},
{ {
"name": "Publish in CI/CD", "name": "Publish in CI/CD",
"path": "/recipes/nx-release/publish-in-ci-cd", "path": "/recipes/nx-release/publish-in-ci-cd",
@ -4140,6 +4148,14 @@
"children": [], "children": [],
"disableCollapsible": false "disableCollapsible": false
}, },
{
"name": "Configure Custom Registries",
"path": "/recipes/nx-release/configure-custom-registries",
"id": "configure-custom-registries",
"isExternal": false,
"children": [],
"disableCollapsible": false
},
{ {
"name": "Publish in CI/CD", "name": "Publish in CI/CD",
"path": "/recipes/nx-release/publish-in-ci-cd", "path": "/recipes/nx-release/publish-in-ci-cd",
@ -4207,6 +4223,14 @@
"children": [], "children": [],
"disableCollapsible": false "disableCollapsible": false
}, },
{
"name": "Configure Custom Registries",
"path": "/recipes/nx-release/configure-custom-registries",
"id": "configure-custom-registries",
"isExternal": false,
"children": [],
"disableCollapsible": false
},
{ {
"name": "Publish in CI/CD", "name": "Publish in CI/CD",
"path": "/recipes/nx-release/publish-in-ci-cd", "path": "/recipes/nx-release/publish-in-ci-cd",

View File

@ -3145,6 +3145,17 @@
"path": "/recipes/nx-release/automatically-version-with-conventional-commits", "path": "/recipes/nx-release/automatically-version-with-conventional-commits",
"tags": ["nx-release"] "tags": ["nx-release"]
}, },
{
"id": "configure-custom-registries",
"name": "Configure Custom Registries",
"description": "",
"mediaImage": "",
"file": "shared/recipes/nx-release/configure-custom-registries",
"itemList": [],
"isExternal": false,
"path": "/recipes/nx-release/configure-custom-registries",
"tags": ["nx-release"]
},
{ {
"id": "publish-in-ci-cd", "id": "publish-in-ci-cd",
"name": "Publish in CI/CD", "name": "Publish in CI/CD",
@ -5668,6 +5679,17 @@
"path": "/recipes/nx-release/automatically-version-with-conventional-commits", "path": "/recipes/nx-release/automatically-version-with-conventional-commits",
"tags": ["nx-release"] "tags": ["nx-release"]
}, },
{
"id": "configure-custom-registries",
"name": "Configure Custom Registries",
"description": "",
"mediaImage": "",
"file": "shared/recipes/nx-release/configure-custom-registries",
"itemList": [],
"isExternal": false,
"path": "/recipes/nx-release/configure-custom-registries",
"tags": ["nx-release"]
},
{ {
"id": "publish-in-ci-cd", "id": "publish-in-ci-cd",
"name": "Publish in CI/CD", "name": "Publish in CI/CD",
@ -5761,6 +5783,17 @@
"path": "/recipes/nx-release/automatically-version-with-conventional-commits", "path": "/recipes/nx-release/automatically-version-with-conventional-commits",
"tags": ["nx-release"] "tags": ["nx-release"]
}, },
"/recipes/nx-release/configure-custom-registries": {
"id": "configure-custom-registries",
"name": "Configure Custom Registries",
"description": "",
"mediaImage": "",
"file": "shared/recipes/nx-release/configure-custom-registries",
"itemList": [],
"isExternal": false,
"path": "/recipes/nx-release/configure-custom-registries",
"tags": ["nx-release"]
},
"/recipes/nx-release/publish-in-ci-cd": { "/recipes/nx-release/publish-in-ci-cd": {
"id": "publish-in-ci-cd", "id": "publish-in-ci-cd",
"name": "Publish in CI/CD", "name": "Publish in CI/CD",

View File

@ -532,6 +532,13 @@
"name": "Automatically Version with Conventional Commits", "name": "Automatically Version with Conventional Commits",
"path": "/recipes/nx-release/automatically-version-with-conventional-commits" "path": "/recipes/nx-release/automatically-version-with-conventional-commits"
}, },
{
"description": "",
"file": "shared/recipes/nx-release/configure-custom-registries",
"id": "configure-custom-registries",
"name": "Configure Custom Registries",
"path": "/recipes/nx-release/configure-custom-registries"
},
{ {
"description": "", "description": "",
"file": "shared/recipes/nx-release/publish-in-ci-cd", "file": "shared/recipes/nx-release/publish-in-ci-cd",

View File

@ -1136,6 +1136,12 @@
"tags": ["nx-release"], "tags": ["nx-release"],
"file": "shared/recipes/nx-release/automatically-version-with-conventional-commits" "file": "shared/recipes/nx-release/automatically-version-with-conventional-commits"
}, },
{
"name": "Configure Custom Registries",
"id": "configure-custom-registries",
"tags": ["nx-release"],
"file": "shared/recipes/nx-release/configure-custom-registries"
},
{ {
"name": "Publish in CI/CD", "name": "Publish in CI/CD",
"id": "publish-in-ci-cd", "id": "publish-in-ci-cd",

View File

@ -0,0 +1,109 @@
# Configure Custom Registries
To publish JavaScript packages, Nx Release uses the `npm` CLI under the hood, which defaults to publishing to the `npm` registry (`https://registry.npmjs.org/`). If you need to publish to a different registry, you can configure the registry in the `.npmrc` file in the root of your workspace or at the project level in the project configuration.
## Set the Registry in the Root .npmrc File
The easiest way to configure a custom registry is to set it in the `npm` configuration via the root `.npmrc` file. This file is located in the root of your workspace, and Nx Release will use it for publishing all projects. To set the registry, add the 'registry' property to your root `.npmrc` file:
```bash .npmrc
registry=https://my-custom-registry.com/
```
### Authenticate to the Registry in CI
To authenticate with a custom registry in CI, you can add authentication tokens to the `.npmrc` file:
```bash .npmrc
registry=https://my-custom-registry.com/
//my-custom-registry.com/:_authToken=<TOKEN>
```
See the [npm documentation](https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#auth-related-configuration) for more information.
## Configure Multiple Registries
The recommended way to determine which registry packages are published to is by using [npm scopes](https://docs.npmjs.com/cli/v10/using-npm/scope). All packages with a name that starts with your scope will be published to the registry specified in the `.npmrc` file for that scope. Consider the following example:
```bash .npmrc
@my-scope:registry=https://my-custom-registry.com/
//my-custom-registry.com/:_authToken=<TOKEN>
@other-scope:registry=https://my-other-registry.com/
//my-other-registry.com/:_authToken=<OTHER_TOKEN>
registry=https://my-default-registry.com/
//my-default-registry.com/:_authToken=<DEFAULT_TOKEN>
```
With the above `.npmrc`, the following packages would be published to the specified registries:
- `@my-scope/pkg-1` -> `https://my-custom-registry.com/`
- `@other-scope/pkg-2` -> `https://my-other-registry.com/`
- `pkg-3` -> `https://my-default-registry.com/`
## Specify an Alternate Registry for a Single Package
In some cases, you may want to configure the registry on a per-package basis instead of by scope. This can be done by setting options in the project's configuration.
{% callout type="info" title="Authentication" %}
All registries set for specific packages must still have authentication tokens set in the root `.npmrc` file for publishing in CI. See [Authenticate to the Registry in CI](#authenticate-to-the-registry-in-ci) for an example.
{% /callout %}
### Set the Registry in the Project Configuration
The project configuration for Nx Release is in two parts - one for the version step and one for the publish step.
#### Update the Version Step
The version step of Nx Release is responsible for determining the new version of the package. If you have set the `version.generatorOptions.currentVersionResolver` to 'registry', then Nx Release will check the remote registry for the current version of the package.
**Note:** If you do not use the 'registry' current version resolver, then this step is not needed.
To set custom registry options for the current version lookup, add the registry and/or tag to the `currentVersionResolverMetadata` in the project configuration:
```json project.json
{
"name": "pkg-5",
"sourceRoot": "...",
"targets": {
...
},
"release": {
"version": {
"generatorOptions": {
"currentVersionResolverMetadata": {
"registry": "https://my-unique-registry.com/",
"tag": "next"
}
}
}
}
}
```
#### Update the Publish Step
The publish step of Nx Release is responsible for publishing the package to the registry. To set custom registry options for publishing, you can add the `registry` and/or `tag` options for the `nx-release-publish` target in the project configuration:
```json project.json
{
"name": "pkg-5",
"sourceRoot": "...",
"targets": {
...,
"nx-release-publish": {
"options": {
"registry": "https://my-unique-registry.com/",
"tag": "next"
}
}
}
}
```
### Set the Registry in the Package Manifest
{% callout type="caution" title="Caution" %}
It is not recommended to set the registry for a package in the 'publishConfig' property of its 'package.json' file. 'npm publish' will always prefer the registry from the 'publishConfig' over the '--registry' argument. Because of this, the '--registry' CLI and programmatic API options of Nx Release will no longer be able to override the registry for purposes such as publishing locally for end to end testing.
{% /callout %}

View File

@ -180,6 +180,7 @@
- [Get Started with Nx Release](/recipes/nx-release/get-started-with-nx-release) - [Get Started with Nx Release](/recipes/nx-release/get-started-with-nx-release)
- [Release Projects Independently](/recipes/nx-release/release-projects-independently) - [Release Projects Independently](/recipes/nx-release/release-projects-independently)
- [Automatically Version with Conventional Commits](/recipes/nx-release/automatically-version-with-conventional-commits) - [Automatically Version with Conventional Commits](/recipes/nx-release/automatically-version-with-conventional-commits)
- [Configure Custom Registries](/recipes/nx-release/configure-custom-registries)
- [Publish in CI/CD](/recipes/nx-release/publish-in-ci-cd) - [Publish in CI/CD](/recipes/nx-release/publish-in-ci-cd)
- [Automate GitHub Releases](/recipes/nx-release/automate-github-releases) - [Automate GitHub Releases](/recipes/nx-release/automate-github-releases)
- [Publish Rust Crates](/recipes/nx-release/publish-rust-crates) - [Publish Rust Crates](/recipes/nx-release/publish-rust-crates)

View File

@ -0,0 +1,430 @@
import { NxJsonConfiguration, ProjectConfiguration } from '@nx/devkit';
import {
cleanupProject,
createFile,
killProcessAndPorts,
newProject,
runCLI,
runCommandUntil,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { execSync } from 'child_process';
import type { PackageJson } from 'nx/src/utils/package-json';
describe('nx release - custom npm registries', () => {
const verdaccioPort = 7191;
const customRegistryUrl = `http://localhost:${verdaccioPort}`;
const scope = 'scope';
beforeAll(async () => {
newProject({
unsetProjectNameAndRootFormat: false,
packages: ['@nx/js'],
});
}, 60000);
afterAll(() => cleanupProject());
it('should respect registry configuration for each package', async () => {
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
nxJson.release = {
projectsRelationship: 'independent',
};
return nxJson;
});
const e2eRegistryUrl = execSync('npm config get registry')
.toString()
.trim();
const npmrcEntries = [
`@${scope}:registry=http://scoped-registry.com`,
'tag=next',
// We can't test overriding the default registry in this file since our e2e tests override it anyway.
// Instead, we'll just assert that the e2e registry is used anytime we expect the default registry
];
createFile('.npmrc', npmrcEntries.join('\n'));
const scopedWithPublishConfig = newPackage('pkg-scoped-publish-config', {
scoped: true,
publishConfig: {
[`@${scope}:registry`]: 'http://publish-config-registry.com',
},
});
const publishResultScopedWithPublishConfig = runCLI(
`release publish -p ${scopedWithPublishConfig} --dry-run`
);
expect(publishResultScopedWithPublishConfig).toContain(
'Would publish to http://publish-config-registry.com with tag "next"'
);
const scopedWithPublishConfigAndProjectConfig = newPackage(
'pkg-scoped-publish-config-project-config',
{
scoped: true,
publishConfig: {
[`@${scope}:registry`]: 'http://publish-config-registry.com',
},
projectConfig: {
registry: 'http://ignored-registry.com',
tag: 'alpha',
},
}
);
const publishResultScopedWithPublishConfigAndProjectConfig = runCLI(
`release publish -p ${scopedWithPublishConfigAndProjectConfig} --dry-run`
);
expect(publishResultScopedWithPublishConfigAndProjectConfig).toContain(
'Would publish to http://publish-config-registry.com with tag "alpha"'
);
const publishResultScopedWithPublishConfigAndProjectConfigAndArg = runCLI(
`release publish -p ${scopedWithPublishConfigAndProjectConfig} --dry-run --registry=http://ignored-registry.com`
);
expect(
publishResultScopedWithPublishConfigAndProjectConfigAndArg
).toContain(
'Would publish to http://publish-config-registry.com with tag "alpha"'
);
const scopedNoOtherConfig = newPackage('pkg-scoped-no-other-config', {
scoped: true,
});
const publishResultScopedNoOtherConfig = runCLI(
`release publish -p ${scopedNoOtherConfig} --dry-run`
);
expect(publishResultScopedNoOtherConfig).toContain(
'Would publish to http://scoped-registry.com with tag "next"'
);
const publishResultScopedWithRegistryArg = runCLI(
`release publish -p ${scopedNoOtherConfig} --dry-run --registry=http://scope-override-registry.com`
);
expect(publishResultScopedWithRegistryArg).toContain(
'Would publish to http://scope-override-registry.com with tag "next"'
);
const noScopeWithPublishConfigAndProjectConfig = newPackage(
'pkg-no-scope-publish-config-project-config',
{
publishConfig: {
registry: 'http://publish-config-registry.com',
},
projectConfig: {
registry: 'http://ignored-registry.com',
tag: 'alpha',
},
}
);
const publishResultNoScopeWithPublishConfigAndProjectConfig = runCLI(
`release publish -p ${noScopeWithPublishConfigAndProjectConfig} --dry-run`
);
expect(publishResultNoScopeWithPublishConfigAndProjectConfig).toContain(
'Would publish to http://publish-config-registry.com with tag "alpha"'
);
const publishResultNoScopeWithPublishConfigAndProjectConfigAndArg = runCLI(
`release publish -p ${noScopeWithPublishConfigAndProjectConfig} --dry-run --registry=http://ignored-registry.com --tag=beta`
);
expect(
publishResultNoScopeWithPublishConfigAndProjectConfigAndArg
).toContain(
`Would publish to http://publish-config-registry.com with tag "beta"`
);
const noScopeNoOtherConfig = newPackage('pkg-no-scope-no-config', {});
const publishResultNoScope = runCLI(
`release publish -p ${noScopeNoOtherConfig} --dry-run`
);
expect(publishResultNoScope).toContain(
`Would publish to ${e2eRegistryUrl} with tag "next"`
);
const publishResultNoScopeWithRegistryArg = runCLI(
`release publish -p ${noScopeNoOtherConfig} --dry-run --registry=${customRegistryUrl} --tag=alpha`
);
expect(publishResultNoScopeWithRegistryArg).toContain(
`Would publish to ${customRegistryUrl} with tag "alpha"`
);
const scopeWithProjectConfig = newPackage('pkg-scope-project-config', {
scoped: true,
projectConfig: {
registry: 'http://scope-override-registry.com',
tag: 'alpha',
},
});
const publishResultScopedWithProjectConfig = runCLI(
`release publish -p ${scopeWithProjectConfig} --dry-run`
);
expect(publishResultScopedWithProjectConfig).toContain(
'Would publish to http://scope-override-registry.com with tag "alpha"'
);
const publishResultScopedWithProjectConfigAndArg = runCLI(
`release publish -p ${scopeWithProjectConfig} --dry-run --registry=http://scope-override-arg-registry.com --tag=prev`
);
expect(publishResultScopedWithProjectConfigAndArg).toContain(
'Would publish to http://scope-override-arg-registry.com with tag "prev"'
);
const noScopeWithProjectConfig = newPackage('pkg-no-scope-project-config', {
projectConfig: {
registry: 'http://default-override-registry.com',
tag: 'alpha',
},
});
const publishResultNoScopeWithProjectConfig = runCLI(
`release publish -p ${noScopeWithProjectConfig} --dry-run`
);
expect(publishResultNoScopeWithProjectConfig).toContain(
'Would publish to http://default-override-registry.com with tag "alpha"'
);
const publishResultNoScopeWithProjectConfigAndArg = runCLI(
`release publish -p ${noScopeWithProjectConfig} --dry-run --registry=http://default-override-arg-registry.com --tag=prev`
);
expect(publishResultNoScopeWithProjectConfigAndArg).toContain(
'Would publish to http://default-override-arg-registry.com with tag "prev"'
);
runCLI(`generate setup-verdaccio`);
const process = await runCommandUntil(
`local-registry @proj/source --port=${verdaccioPort}`,
(output) => output.includes(`warn --- http address`)
);
const npmrcEntries2 = [
`@${scope}:registry=${customRegistryUrl}`,
`registry=http://ignored-registry.com`,
'tag=next',
];
updateFile('.npmrc', npmrcEntries2.join('\n'));
const actualScopedWithPublishConfigAndProjectConfig = newPackage(
'pkg-actual-scoped-publish-config-project-config',
{
scoped: true,
publishConfig: {
[`@${scope}:registry`]: e2eRegistryUrl,
},
projectConfig: {
registry: 'http://ignored-registry.com',
tag: 'beta',
version: {
tag: 'alpha', // alpha tag will be passed via publish CLI arg to override the above 'beta'
},
},
}
);
const actualPublishResultScoped = runCLI(
`release publish -p ${actualScopedWithPublishConfigAndProjectConfig} --registry=http://ignored-registry.com --tag=alpha`
);
const actualScopedWithWrongPublishConfig = newPackage(
'pkg-actual-scoped-wrong-publish-config',
{
scoped: true,
publishConfig: {
// to properly override the registry for a scoped package, the key needs to include the scope
registry: 'http://ignored-registry.com',
},
projectConfig: {}, // this will still set the current version resolver to 'registry', it just won't set the registry url
}
);
const actualPublishResultScopedWithWrongPublishConfig = runCLI(
`release publish -p ${actualScopedWithWrongPublishConfig}`
);
const actualNoScopeWithProjectConfig = newPackage(
'pkg-actual-no-scope-project-config',
{
projectConfig: {
registry: customRegistryUrl,
tag: 'beta',
},
}
);
const actualPublishResultNoScopeWithProjectConfig = runCLI(
`release publish -p ${actualNoScopeWithProjectConfig}`
);
const actualScopedWithProjectConfig = newPackage(
'pkg-actual-scoped-project-config',
{
scoped: true,
projectConfig: {
registry: e2eRegistryUrl,
tag: 'prev',
},
}
);
const actualPublishResultScopedWithProjectConfig = runCLI(
`release publish -p ${actualScopedWithProjectConfig}`
);
const versionResult = runCLI(
`release version 999.9.9 -p "${actualScopedWithPublishConfigAndProjectConfig},${actualScopedWithWrongPublishConfig},${actualNoScopeWithProjectConfig},${actualScopedWithProjectConfig}" --dry-run`,
{ silenceError: true } // don't error on this command because the verdaccio process needs to be killed regardless before the test exits
);
await killProcessAndPorts(process.pid, verdaccioPort);
expect(actualPublishResultScoped).toContain(
`Published to ${e2eRegistryUrl} with tag "alpha"`
);
expect(actualPublishResultScopedWithWrongPublishConfig).toContain(
`Published to ${customRegistryUrl} with tag "next"`
);
expect(actualPublishResultNoScopeWithProjectConfig).toContain(
`Published to ${customRegistryUrl} with tag "beta"`
);
expect(actualPublishResultScopedWithProjectConfig).toContain(
`Published to ${e2eRegistryUrl} with tag "prev"`
);
expect(
versionResult.match(
new RegExp(
`Resolved the current version as 0.0.0 for tag "alpha" from registry ${e2eRegistryUrl}`,
'g'
)
).length
).toBe(1);
expect(
versionResult.match(
new RegExp(
`Resolved the current version as 0.0.0 for tag "next" from registry ${customRegistryUrl}`,
'g'
)
).length
).toBe(1);
expect(
versionResult.match(
new RegExp(
`Resolved the current version as 0.0.0 for tag "beta" from registry ${customRegistryUrl}`,
'g'
)
).length
).toBe(1);
expect(
versionResult.match(
new RegExp(
`Resolved the current version as 0.0.0 for tag "prev" from registry ${e2eRegistryUrl}`,
'g'
)
).length
).toBe(1);
}, 600000);
function newPackage(
name: string,
options: {
scoped?: true;
publishConfig?: Record<string, string>;
projectConfig?: {
registry?: string;
tag?: string;
version?: {
registry?: string;
tag?: string;
};
publish?: {
registry?: string;
tag?: string;
};
};
}
): string {
const projectName = uniq(name);
runCLI(`generate @nx/workspace:npm-package ${projectName}`);
let packageName = projectName;
if (options.scoped) {
packageName = `@${scope}/${projectName}`;
}
updateJson<PackageJson>(`${projectName}/package.json`, (json) => {
json.name = packageName;
if (options.publishConfig) {
json.publishConfig = options.publishConfig;
}
return json;
});
updateJson<ProjectConfiguration>(`${projectName}/project.json`, (json) => {
if (options.projectConfig) {
json.release = {
version: {
generatorOptions: {
currentVersionResolver: 'registry',
currentVersionResolverMetadata: {},
},
},
};
json.targets = {
...json.targets,
'nx-release-publish': {
options: {},
},
};
}
if (options.projectConfig?.registry) {
json.targets['nx-release-publish'].options.registry =
options.projectConfig.registry;
(
json.release.version.generatorOptions
.currentVersionResolverMetadata as Record<string, string>
).registry = options.projectConfig.registry;
}
if (options.projectConfig?.tag) {
json.targets['nx-release-publish'].options.tag =
options.projectConfig.tag;
(
json.release.version.generatorOptions
.currentVersionResolverMetadata as Record<string, string>
).tag = options.projectConfig.tag;
}
if (options.projectConfig?.publish?.registry) {
json.targets['nx-release-publish'].options.registry =
options.projectConfig.publish.registry;
}
if (options.projectConfig?.publish?.tag) {
json.targets['nx-release-publish'].options.tag =
options.projectConfig.publish.tag;
}
if (options.projectConfig?.version?.registry) {
(
json.release.version.generatorOptions
.currentVersionResolverMetadata as Record<string, string>
).registry = options.projectConfig.version.registry;
}
if (options.projectConfig?.version?.tag) {
(
json.release.version.generatorOptions
.currentVersionResolverMetadata as Record<string, string>
).tag = options.projectConfig.version.tag;
}
return json;
});
return projectName;
}
});

View File

@ -1,6 +1,8 @@
import { ExecutorContext, joinPathFragments, readJsonFile } from '@nx/devkit'; import { ExecutorContext, readJsonFile } from '@nx/devkit';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { env as appendLocalEnv } from 'npm-run-path'; import { env as appendLocalEnv } from 'npm-run-path';
import { join } from 'path';
import { parseRegistryOptions } from '../../utils/npm-config';
import { logTar } from './log-tar'; import { logTar } from './log-tar';
import { PublishExecutorSchema } from './schema'; import { PublishExecutorSchema } from './schema';
import chalk = require('chalk'); import chalk = require('chalk');
@ -32,14 +34,14 @@ export default async function runExecutor(
const projectConfig = const projectConfig =
context.projectsConfigurations!.projects[context.projectName!]!; context.projectsConfigurations!.projects[context.projectName!]!;
const packageRoot = joinPathFragments( const packageRoot = join(
context.root, context.root,
options.packageRoot ?? projectConfig.root options.packageRoot ?? projectConfig.root
); );
const packageJsonPath = joinPathFragments(packageRoot, 'package.json'); const packageJsonPath = join(packageRoot, 'package.json');
const projectPackageJson = readJsonFile(packageJsonPath); const packageJson = readJsonFile(packageJsonPath);
const packageName = projectPackageJson.name; const packageName = packageJson.name;
// If package and project name match, we can make log messages terser // If package and project name match, we can make log messages terser
let packageTxt = let packageTxt =
@ -47,7 +49,7 @@ export default async function runExecutor(
? `package "${packageName}"` ? `package "${packageName}"`
: `package "${packageName}" from project "${context.projectName}"`; : `package "${packageName}" from project "${context.projectName}"`;
if (projectPackageJson.private === true) { if (packageJson.private === true) {
console.warn( console.warn(
`Skipped ${packageTxt}, because it has \`"private": true\` in ${packageJsonPath}` `Skipped ${packageTxt}, because it has \`"private": true\` in ${packageJsonPath}`
); );
@ -56,32 +58,28 @@ export default async function runExecutor(
}; };
} }
const npmPublishCommandSegments = [`npm publish --json`]; const warnFn = (message: string) => {
console.log(chalk.keyword('orange')(message));
};
const { registry, tag, registryConfigKey } = await parseRegistryOptions(
context.root,
{
packageRoot,
packageJson,
},
{
registry: options.registry,
tag: options.tag,
},
warnFn
);
const npmViewCommandSegments = [ const npmViewCommandSegments = [
`npm view ${packageName} versions dist-tags --json`, `npm view ${packageName} versions dist-tags --json --"${registryConfigKey}=${registry}"`,
];
const npmDistTagAddCommandSegments = [
`npm dist-tag add ${packageName}@${packageJson.version} ${tag} --"${registryConfigKey}=${registry}"`,
]; ];
if (options.registry) {
npmPublishCommandSegments.push(`--registry=${options.registry}`);
npmViewCommandSegments.push(`--registry=${options.registry}`);
}
if (options.tag) {
npmPublishCommandSegments.push(`--tag=${options.tag}`);
}
if (options.otp) {
npmPublishCommandSegments.push(`--otp=${options.otp}`);
}
if (isDryRun) {
npmPublishCommandSegments.push(`--dry-run`);
}
// 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();
/** /**
* In a dry-run scenario, it is most likely that all commands are being run with dry-run, therefore * In a dry-run scenario, it is most likely that all commands are being run with dry-run, therefore
@ -92,11 +90,11 @@ export default async function runExecutor(
* perform the npm view step, and just show npm publish's dry-run output. * perform the npm view step, and just show npm publish's dry-run output.
*/ */
if (!isDryRun && !options.firstRelease) { if (!isDryRun && !options.firstRelease) {
const currentVersion = projectPackageJson.version; const currentVersion = packageJson.version;
try { try {
const result = execSync(npmViewCommandSegments.join(' '), { const result = execSync(npmViewCommandSegments.join(' '), {
env: processEnv(true), env: processEnv(true),
cwd: packageRoot, cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
}); });
@ -114,14 +112,11 @@ export default async function runExecutor(
if (resultJson.versions.includes(currentVersion)) { if (resultJson.versions.includes(currentVersion)) {
try { try {
if (!isDryRun) { if (!isDryRun) {
execSync( execSync(npmDistTagAddCommandSegments.join(' '), {
`npm dist-tag add ${packageName}@${currentVersion} ${tag} --registry=${registry}`,
{
env: processEnv(true), env: processEnv(true),
cwd: packageRoot, cwd: context.root,
stdio: 'ignore', stdio: 'ignore',
} });
);
console.log( console.log(
`Added the dist-tag ${tag} to v${currentVersion} for registry ${registry}.\n` `Added the dist-tag ${tag} to v${currentVersion} for registry ${registry}.\n`
); );
@ -205,11 +200,23 @@ export default async function runExecutor(
console.log('Skipped npm view because --first-release was set'); console.log('Skipped npm view because --first-release was set');
} }
const npmPublishCommandSegments = [
`npm publish ${packageRoot} --json --"${registryConfigKey}=${registry}" --tag=${tag}`,
];
if (options.otp) {
npmPublishCommandSegments.push(`--otp=${options.otp}`);
}
if (isDryRun) {
npmPublishCommandSegments.push(`--dry-run`);
}
try { try {
const output = execSync(npmPublishCommandSegments.join(' '), { const output = execSync(npmPublishCommandSegments.join(' '), {
maxBuffer: LARGE_BUFFER, maxBuffer: LARGE_BUFFER,
env: processEnv(true), env: processEnv(true),
cwd: packageRoot, cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
}); });

View File

@ -2,7 +2,6 @@ import {
ProjectGraphProjectNode, ProjectGraphProjectNode,
Tree, Tree,
formatFiles, formatFiles,
joinPathFragments,
output, output,
readJson, readJson,
updateJson, updateJson,
@ -11,7 +10,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import * as chalk from 'chalk'; import * as chalk from 'chalk';
import { exec } from 'node:child_process'; import { exec } from 'node:child_process';
import { relative } from 'node:path'; import { join } from 'node:path';
import { IMPLICIT_DEFAULT_RELEASE_GROUP } from 'nx/src/command-line/release/config/config'; import { IMPLICIT_DEFAULT_RELEASE_GROUP } from 'nx/src/command-line/release/config/config';
import { import {
getFirstGitCommit, getFirstGitCommit,
@ -31,6 +30,7 @@ import {
import { interpolate } from 'nx/src/tasks-runner/utils'; import { interpolate } from 'nx/src/tasks-runner/utils';
import * as ora from 'ora'; import * as ora from 'ora';
import { prerelease } from 'semver'; import { prerelease } from 'semver';
import { parseRegistryOptions } from '../../utils/npm-config';
import { ReleaseVersionGeneratorSchema } from './schema'; import { ReleaseVersionGeneratorSchema } from './schema';
import { resolveLocalPackageDependencies } from './utils/resolve-local-package-dependencies'; import { resolveLocalPackageDependencies } from './utils/resolve-local-package-dependencies';
import { updateLockFile } from './utils/update-lock-file'; import { updateLockFile } from './utils/update-lock-file';
@ -111,11 +111,7 @@ Valid values are: ${validReleaseVersionPrefixes
); );
} }
const packageJsonPath = joinPathFragments(packageRoot, 'package.json'); const packageJsonPath = join(packageRoot, 'package.json');
const workspaceRelativePackageJsonPath = relative(
workspaceRoot,
packageJsonPath
);
const color = getColor(projectName); const color = getColor(projectName);
const log = (msg: string) => { const log = (msg: string) => {
@ -124,7 +120,7 @@ Valid values are: ${validReleaseVersionPrefixes
if (!tree.exists(packageJsonPath)) { if (!tree.exists(packageJsonPath)) {
throw new Error( throw new Error(
`The project "${projectName}" does not have a package.json available at ${workspaceRelativePackageJsonPath}. `The project "${projectName}" does not have a package.json available at ${packageJsonPath}.
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.` 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.`
); );
@ -136,22 +132,40 @@ To fix this you will either need to add a package.json file at that location, or
)}` )}`
); );
const projectPackageJson = readJson(tree, packageJsonPath); const packageJson = readJson(tree, packageJsonPath);
log( log(
`🔍 Reading data for package "${projectPackageJson.name}" from ${workspaceRelativePackageJsonPath}` `🔍 Reading data for package "${packageJson.name}" from ${packageJsonPath}`
); );
const { name: packageName, version: currentVersionFromDisk } = const { name: packageName, version: currentVersionFromDisk } =
projectPackageJson; packageJson;
switch (options.currentVersionResolver) { switch (options.currentVersionResolver) {
case 'registry': { case 'registry': {
const metadata = options.currentVersionResolverMetadata; const metadata = options.currentVersionResolverMetadata;
const registry = const registryArg =
metadata?.registry ?? typeof metadata?.registry === 'string'
(await getNpmRegistry()) ?? ? metadata.registry
'https://registry.npmjs.org'; : undefined;
const tag = metadata?.tag ?? 'latest'; const tagArg =
typeof metadata?.tag === 'string' ? metadata.tag : undefined;
const warnFn = (message: string) => {
console.log(chalk.keyword('orange')(message));
};
const { registry, tag, registryConfigKey } =
await parseRegistryOptions(
workspaceRoot,
{
packageRoot: join(workspaceRoot, packageRoot),
packageJson,
},
{
registry: registryArg,
tag: tagArg,
},
warnFn
);
/** /**
* If the currentVersionResolver is set to registry, and the projects are not independent, we only want to make the request once for the whole batch of projects. * If the currentVersionResolver is set to registry, and the projects are not independent, we only want to make the request once for the whole batch of projects.
@ -174,7 +188,7 @@ To fix this you will either need to add a package.json file at that location, or
// Must be non-blocking async to allow spinner to render // Must be non-blocking async to allow spinner to render
currentVersion = await new Promise<string>((resolve, reject) => { currentVersion = await new Promise<string>((resolve, reject) => {
exec( exec(
`npm view ${packageName} version --registry=${registry} --tag=${tag}`, `npm view ${packageName} version --"${registryConfigKey}=${registry}" --tag=${tag}`,
(error, stdout, stderr) => { (error, stdout, stderr) => {
if (error) { if (error) {
return reject(error); return reject(error);
@ -429,7 +443,7 @@ To fix this you will either need to add a package.json file at that location, or
if (!specifier) { if (!specifier) {
log( log(
`🚫 Skipping versioning "${projectPackageJson.name}" as no changes were detected.` `🚫 Skipping versioning "${packageJson.name}" as no changes were detected.`
); );
continue; continue;
} }
@ -442,13 +456,11 @@ To fix this you will either need to add a package.json file at that location, or
versionData[projectName].newVersion = newVersion; versionData[projectName].newVersion = newVersion;
writeJson(tree, packageJsonPath, { writeJson(tree, packageJsonPath, {
...projectPackageJson, ...packageJson,
version: newVersion, version: newVersion,
}); });
log( log(`✍️ New version ${newVersion} written to ${packageJsonPath}`);
`✍️ New version ${newVersion} written to ${workspaceRelativePackageJsonPath}`
);
if (dependentProjects.length > 0) { if (dependentProjects.length > 0) {
log( log(
@ -471,10 +483,7 @@ To fix this you will either need to add a package.json file at that location, or
`The dependent project "${dependentProject.source}" does not have a packageRoot available. Please report this issue on https://github.com/nrwl/nx` `The dependent project "${dependentProject.source}" does not have a packageRoot available. Please report this issue on https://github.com/nrwl/nx`
); );
} }
updateJson( updateJson(tree, join(dependentPackageRoot, 'package.json'), (json) => {
tree,
joinPathFragments(dependentPackageRoot, 'package.json'),
(json) => {
// Auto (i.e.infer existing) by default // Auto (i.e.infer existing) by default
let versionPrefix = options.versionPrefix ?? 'auto'; let versionPrefix = options.versionPrefix ?? 'auto';
@ -497,8 +506,7 @@ To fix this you will either need to add a package.json file at that location, or
packageName packageName
] = `${versionPrefix}${newVersion}`; ] = `${versionPrefix}${newVersion}`;
return json; return json;
} });
);
} }
} }
@ -571,18 +579,3 @@ function getColor(projectName: string) {
return colors[colorIndex]; return colors[colorIndex];
} }
async function getNpmRegistry() {
// Must be non-blocking async to allow spinner to render
return await new Promise<string>((resolve, reject) => {
exec('npm config get registry', (error, stdout, stderr) => {
if (error) {
return reject(error);
}
if (stderr) {
return reject(stderr);
}
return resolve(stdout.trim());
});
});
}

View File

@ -0,0 +1,328 @@
import { ExecException } from 'child_process';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { PackageJson } from 'nx/src/utils/package-json';
import { join } from 'path';
import { getNpmRegistry, getNpmTag, parseRegistryOptions } from './npm-config';
jest.mock('child_process', () => {
const original = jest.requireActual('child_process');
return {
...original,
exec: jest
.fn()
.mockImplementation(
(
command: string,
_: unknown,
callback: (
error: ExecException,
stdout: string,
stderr: string
) => void
) => {
switch (command) {
case 'npm config get @scope:registry':
callback(null, 'https://scoped-registry.com', null);
break;
case 'npm config get @missing:registry':
callback(null, 'undefined', null);
break;
case 'npm config get registry':
callback(null, 'https://custom-registry.com', null);
break;
case 'npm config get tag':
callback(null, 'next', null);
break;
default:
callback(
new Error(`unexpected command: ${command}`),
null,
'ERROR'
);
}
}
),
};
});
describe('npm-config', () => {
let tempFs: TempFs;
beforeEach(() => {
tempFs = new TempFs('npm-config');
});
describe('getNpmRegistry', () => {
it('should return scoped registry if it exists', async () => {
const registry = await getNpmRegistry(tempFs.tempDir, '@scope');
expect(registry).toEqual('https://scoped-registry.com');
});
it('should return registry if scoped registry does not exist', async () => {
const registry = await getNpmRegistry(tempFs.tempDir, '@missing');
expect(registry).toEqual('https://custom-registry.com');
});
it('should return registry if package is not scoped', async () => {
const registry = await getNpmRegistry(tempFs.tempDir);
expect(registry).toEqual('https://custom-registry.com');
});
});
describe('getNpmTag', () => {
it('should return tag from npm config', async () => {
const tag = await getNpmTag(tempFs.tempDir);
expect(tag).toEqual('next');
});
});
describe('parseRegistryOptions', () => {
let logMessage: string;
const logFn = (message: string) => {
logMessage += message;
};
beforeEach(() => {
logMessage = '';
});
it('should warn if .npmrc exists in the package root', async () => {
await tempFs.createFile(
join('packages', 'pkg1', '.npmrc'),
'registry=https://custom-registry.com'
);
await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: join(tempFs.tempDir, 'packages', 'pkg1'),
packageJson: {
name: 'pkg1',
} as PackageJson,
},
{},
logFn
);
expect(logMessage).toContain(
'Ignoring .npmrc file detected in the package root'
);
});
it('should warn and return registry set in publishConfig', async () => {
const { registry, registryConfigKey } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: 'pkg1',
publishConfig: {
registry: 'https://publish-config.com',
} as PackageJson['publishConfig'],
} as PackageJson,
},
{},
logFn
);
expect(logMessage).toContain("Registry detected in the 'publishConfig'");
expect(logMessage).toContain(
'prevents the registry from being overridden'
);
expect(registry).toEqual('https://publish-config.com');
expect(registryConfigKey).toEqual('registry');
});
it('should warn and return registry set in publishConfig instead of registry arg', async () => {
const { registry, registryConfigKey } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: 'pkg1',
publishConfig: {
registry: 'https://publish-config.com',
} as PackageJson['publishConfig'],
} as PackageJson,
},
{
registry: 'https://ignored-registry.com',
},
logFn
);
expect(logMessage).toContain("Registry detected in the 'publishConfig'");
expect(logMessage).toContain('This will override your registry option');
expect(registry).toEqual('https://publish-config.com');
expect(registryConfigKey).toEqual('registry');
});
it('should warn and return scoped registry set in publishConfig instead of registry arg for a scoped package', async () => {
const { registry, registryConfigKey } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: '@scope/pkg1',
publishConfig: {
'@scope:registry': 'https://publish-config.com',
} as PackageJson['publishConfig'],
} as PackageJson,
},
{
registry: 'https://ignored-registry.com',
},
logFn
);
expect(logMessage).toContain("Registry detected in the 'publishConfig'");
expect(registry).toContain('https://publish-config.com');
expect(registryConfigKey).toEqual('@scope:registry');
});
it('should warn if registry is set in publishConfig for a scoped package, but still return registry arg', async () => {
const { registry, registryConfigKey } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: '@scope/pkg1',
publishConfig: {
registry: 'https://publish-config.com',
} as PackageJson['publishConfig'],
} as PackageJson,
},
{
registry: 'https://registry-arg.com',
},
logFn
);
expect(logMessage).toContain("Registry detected in the 'publishConfig'");
expect(registry).toContain('https://registry-arg.com');
expect(registryConfigKey).toEqual('@scope:registry');
});
it('should return registry arg over npm config', async () => {
const { registry, registryConfigKey } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: 'pkg1',
} as PackageJson,
},
{
registry: 'https://registry-arg.com',
},
logFn
);
expect(registry).toEqual('https://registry-arg.com');
expect(registryConfigKey).toEqual('registry');
});
it('should return registry arg over npm config for scoped packages', async () => {
const { registry, registryConfigKey } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: '@scope/pkg1',
} as PackageJson,
},
{
registry: 'https://registry-arg.com',
},
logFn
);
expect(registry).toEqual('https://registry-arg.com');
expect(registryConfigKey).toEqual('@scope:registry');
});
it('should defer to npm config for scoped registry', async () => {
const { registry, registryConfigKey } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: '@scope/pkg1',
} as PackageJson,
},
{},
logFn
);
expect(registry).toEqual('https://scoped-registry.com');
expect(registryConfigKey).toEqual('@scope:registry');
});
it('should defer to npm config for registry if scoped registry does not exist', async () => {
const { registry, registryConfigKey } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: '@missing/pkg1',
} as PackageJson,
},
{},
logFn
);
expect(registry).toEqual('https://custom-registry.com');
expect(registryConfigKey).toEqual('@missing:registry');
});
it('should defer to npm config for registry if package is not scoped', async () => {
const { registry, registryConfigKey } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: 'pkg1',
} as PackageJson,
},
{},
logFn
);
expect(registry).toEqual('https://custom-registry.com');
expect(registryConfigKey).toEqual('registry');
});
it('should return npm tag from config', async () => {
const { tag } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: 'pkg1',
} as PackageJson,
},
{},
logFn
);
expect(tag).toEqual('next');
});
it('should override npm tag when tag option is passed', async () => {
const { tag } = await parseRegistryOptions(
tempFs.tempDir,
{
packageRoot: tempFs.tempDir,
packageJson: {
name: 'pkg1',
} as PackageJson,
},
{
tag: 'alpha',
},
logFn
);
expect(tag).toEqual('alpha');
});
});
});

View File

@ -0,0 +1,121 @@
import { exec } from 'child_process';
import { existsSync } from 'fs';
import { PackageJson } from 'nx/src/utils/package-json';
import { join, relative } from 'path';
export async function parseRegistryOptions(
cwd: string,
pkg: {
packageRoot: string;
packageJson: PackageJson;
},
options: {
registry?: string;
tag?: string;
},
logWarnFn: (message: string) => void = console.warn
): Promise<{ registry: string; tag: string; registryConfigKey: string }> {
const npmRcPath = join(pkg.packageRoot, '.npmrc');
if (existsSync(npmRcPath)) {
const relativeNpmRcPath = relative(cwd, npmRcPath);
logWarnFn(
`\nIgnoring .npmrc file detected in the package root: ${relativeNpmRcPath}. Nested .npmrc files are not supported by npm. Only the .npmrc file at the root of the workspace will be used. To customize the registry or tag for specific packages, see https://nx.dev/recipes/nx-release/configure-custom-registries\n`
);
}
const scope = pkg.packageJson.name.startsWith('@')
? pkg.packageJson.name.split('/')[0]
: '';
// If the package is scoped, then the registry argument that will
// correctly override the registry in the .npmrc file must be scoped.
const registryConfigKey = scope ? `${scope}:registry` : 'registry';
const publishConfigRegistry =
pkg.packageJson.publishConfig?.[registryConfigKey];
// Even though it won't override the actual registry that's actually used,
// the user might think otherwise, so we should still warn if the user has
// set a 'registry' in 'publishConfig' for a scoped package.
if (publishConfigRegistry || pkg.packageJson.publishConfig?.registry) {
const relativePackageJsonPath = relative(
cwd,
join(pkg.packageRoot, 'package.json')
);
if (options.registry) {
logWarnFn(
`\nRegistry detected in the 'publishConfig' of the package manifest: ${relativePackageJsonPath}. This will override your registry option set in the project configuration or passed via the --registry argument, which is why configuring the registry with 'publishConfig' is not recommended. For details, see https://nx.dev/recipes/nx-release/configure-custom-registries\n`
);
} else {
logWarnFn(
`\nRegistry detected in the 'publishConfig' of the package manifest: ${relativePackageJsonPath}. Configuring the registry in this way is not recommended because it prevents the registry from being overridden in project configuration or via the --registry argument. To customize the registry for specific packages, see https://nx.dev/recipes/nx-release/configure-custom-registries\n`
);
}
}
const registry =
// `npm publish` will always use the publishConfig registry if it exists, even over the --registry arg
publishConfigRegistry ||
options.registry ||
(await getNpmRegistry(cwd, scope));
const tag = options.tag || (await getNpmTag(cwd));
return { registry, tag, registryConfigKey };
}
/**
* Returns the npm registry that is used for publishing.
*
* @param scope the scope of the package for which to determine the registry
* @param cwd the directory where the npm config should be read from
*/
export async function getNpmRegistry(
cwd: string,
scope?: string
): Promise<string> {
let registry: string | undefined;
if (scope) {
registry = await getNpmConfigValue(`${scope}:registry`, cwd);
}
if (!registry) {
registry = await getNpmConfigValue('registry', cwd);
}
return registry;
}
/**
* Returns the npm tag that is used for publishing.
*
* @param cwd the directory where the npm config should be read from
*/
export async function getNpmTag(cwd: string): Promise<string> {
// npm does not support '@scope:tag' in the npm config, so we only need to check for 'tag'.
return getNpmConfigValue('tag', cwd);
}
async function getNpmConfigValue(key: string, cwd: string): Promise<string> {
try {
const result = await execAsync(`npm config get ${key}`, cwd);
return result === 'undefined' ? undefined : result;
} catch (e) {
return Promise.resolve(undefined);
}
}
async function execAsync(command: string, cwd: string): Promise<string> {
// Must be non-blocking async to allow spinner to render
return new Promise<string>((resolve, reject) => {
exec(command, { cwd }, (error, stdout, stderr) => {
if (error) {
return reject(error);
}
if (stderr) {
return reject(stderr);
}
return resolve(stdout.trim());
});
});
}

View File

@ -4,9 +4,9 @@ import {
InputDefinition, InputDefinition,
TargetConfiguration, TargetConfiguration,
} from '../config/workspace-json-project-json'; } from '../config/workspace-json-project-json';
import { mergeTargetConfigurations } from '../project-graph/utils/project-configuration-utils';
import { readJsonFile } from './fileutils'; import { readJsonFile } from './fileutils';
import { getNxRequirePaths } from './installation-directory'; import { getNxRequirePaths } from './installation-directory';
import { mergeTargetConfigurations } from '../project-graph/utils/project-configuration-utils';
export interface NxProjectPackageJsonConfiguration { export interface NxProjectPackageJsonConfiguration {
implicitDependencies?: string[]; implicitDependencies?: string[];
@ -59,6 +59,7 @@ export interface PackageJson {
| { | {
packages: string[]; packages: string[];
}; };
publishConfig?: Record<string, string>;
// Nx Project Configuration // Nx Project Configuration
nx?: NxProjectPackageJsonConfiguration; nx?: NxProjectPackageJsonConfiguration;