fix(core): tweaks to nx init (#30002)

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

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

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

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

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

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

Fixes #
This commit is contained in:
James Henry 2025-02-13 00:37:42 +04:00 committed by GitHub
parent 672318de7f
commit 8b1cd482e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 573 additions and 68 deletions

View File

@ -18,6 +18,7 @@ Nx.json configuration
### Properties
- [$schema](../../devkit/documents/NxJsonConfiguration#$schema): string
- [affected](../../devkit/documents/NxJsonConfiguration#affected): NxAffectedConfig
- [cacheDirectory](../../devkit/documents/NxJsonConfiguration#cachedirectory): string
- [cli](../../devkit/documents/NxJsonConfiguration#cli): Object
@ -47,6 +48,12 @@ Nx.json configuration
## Properties
### $schema
`Optional` **$schema**: `string`
---
### affected
`Optional` **affected**: [`NxAffectedConfig`](../../devkit/documents/NxAffectedConfig)

View File

@ -16,6 +16,7 @@ use ProjectsConfigurations or NxJsonConfiguration
### Properties
- [$schema](../../devkit/documents/Workspace#$schema): string
- [affected](../../devkit/documents/Workspace#affected): NxAffectedConfig
- [cacheDirectory](../../devkit/documents/Workspace#cachedirectory): string
- [cli](../../devkit/documents/Workspace#cli): Object
@ -47,6 +48,16 @@ use ProjectsConfigurations or NxJsonConfiguration
## Properties
### $schema
`Optional` **$schema**: `string`
#### Inherited from
[NxJsonConfiguration](../../devkit/documents/NxJsonConfiguration).[$schema](../../devkit/documents/NxJsonConfiguration#$schema)
---
### affected
`Optional` **affected**: [`NxAffectedConfig`](../../devkit/documents/NxAffectedConfig)

View File

@ -54,6 +54,7 @@ export const allowedProjectExtensions = [
// There are some props in here (root) that angular already knows about,
// but it doesn't hurt to have them in here as well to help static analysis.
export const allowedWorkspaceExtensions = [
'$schema',
'implicitDependencies',
'affected',
'defaultBase',

View File

@ -7,13 +7,19 @@ export const yargsInitCommand: CommandModule = {
'Adds Nx to any type of workspace. It installs nx, creates an nx.json configuration file and optionally sets up remote caching. For more info, check https://nx.dev/recipes/adopting-nx.',
builder: (yargs) => withInitOptions(yargs),
handler: async (args: any) => {
const useV2 = await isInitV2();
if (useV2) {
await require('./init-v2').initHandler(args);
} else {
await require('./init-v1').initHandler(args);
try {
const useV2 = await isInitV2();
if (useV2) {
await require('./init-v2').initHandler(args);
} else {
await require('./init-v1').initHandler(args);
}
process.exit(0);
} catch {
// Ensure the cursor is always restored just in case the user has bailed during interactive prompts
process.stdout.write('\x1b[?25h');
process.exit(1);
}
process.exit(0);
},
};

View File

@ -0,0 +1,64 @@
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { readJsonFile, writeJsonFile } from '../../../utils/fileutils';
import { output } from '../../../utils/output';
import { getPackageManagerCommand } from '../../../utils/package-manager';
import { InitArgs } from '../init-v1';
import {
addDepsToPackageJson,
createNxJsonFromTurboJson,
runInstall,
updateGitIgnore,
} from './utils';
type Options = Pick<InitArgs, 'nxCloud' | 'interactive'>;
export async function addNxToTurborepo(_options: Options) {
const repoRoot = process.cwd();
output.log({
title: 'Initializing Nx based on your old Turborepo configuration',
});
output.log({
title: '💡 Did you know?',
bodyLines: [
'- Turborepo requires you to maintain all your common scripts like "build", "lint", "test" in all your packages, as well as their applicable cache inputs and outputs.',
`- Nx is extensible and has plugins for the tools you use to infer all of this for you purely based on that tool's configuration file within your packages.`,
'',
' - E.g. the `@nx/vite` plugin will infer the "build" script based on the existence of a vite.config.js file.',
' - Therefore with zero package level config, `nx build my-app` knows to run the `vite build` CLI directly, with all Nx cache inputs and outputs automatically inferred.',
'',
`NOTE: None of your existing package.json scripts will be modified as part of this initialization process, you can already use them as-is with Nx, but you can learn more about the benefits of Nx's inferred tasks at https://nx.dev/concepts/inferred-tasks`,
],
});
let nxJson = createNxJsonFromTurboJson(readJsonFile('turbo.json'));
const nxJsonPath = join(repoRoot, 'nx.json');
// Turborepo workspaces usually have prettier installed, so try and match the formatting before writing the file
try {
const prettier = await import('prettier');
const config = await prettier.resolveConfig(repoRoot);
writeFileSync(
nxJsonPath,
// @ts-ignore - Always await prettier.format, in modern versions it's async
await prettier.format(JSON.stringify(nxJson, null, 2), {
...(config ?? {}),
parser: 'json',
})
);
} catch (err) {
// Apply fallback JSON write
writeJsonFile(nxJsonPath, nxJson);
}
const pmc = getPackageManagerCommand();
updateGitIgnore(repoRoot);
addDepsToPackageJson(repoRoot);
output.log({ title: '📦 Installing dependencies' });
runInstall(repoRoot, pmc);
}

View File

@ -0,0 +1,46 @@
import { execSync } from 'node:child_process';
import { deduceDefaultBase as gitInitDefaultBase } from '../../../utils/default-base';
export function deduceDefaultBase() {
try {
execSync(`git rev-parse --verify main`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'main';
} catch {
try {
execSync(`git rev-parse --verify dev`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'dev';
} catch {
try {
execSync(`git rev-parse --verify develop`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'develop';
} catch {
try {
execSync(`git rev-parse --verify next`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'next';
} catch {
try {
execSync(`git rev-parse --verify master`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'master';
} catch {
return gitInitDefaultBase();
}
}
}
}
}
}

View File

@ -0,0 +1,269 @@
jest.mock('./deduce-default-base', () => ({
deduceDefaultBase: jest.fn(() => 'main'),
}));
import { NxJsonConfiguration } from '../../../config/nx-json';
import { createNxJsonFromTurboJson } from './utils';
describe('utils', () => {
describe('createNxJsonFromTurboJson', () => {
test.each<{
description: string;
turbo: Record<string, any>;
nx: NxJsonConfiguration;
}>([
{
description: 'empty turbo.json',
turbo: {},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
},
},
{
description: 'global dependencies',
turbo: {
globalDependencies: ['babel.config.json'],
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
namedInputs: {
sharedGlobals: ['{workspaceRoot}/babel.config.json'],
default: ['{projectRoot}/**/*', 'sharedGlobals'],
},
},
},
{
description: 'global env variables',
turbo: {
globalEnv: ['NEXT_PUBLIC_API', 'NODE_ENV'],
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
namedInputs: {
sharedGlobals: [{ env: 'NEXT_PUBLIC_API' }, { env: 'NODE_ENV' }],
default: ['{projectRoot}/**/*', 'sharedGlobals'],
},
},
},
{
description: 'basic task configuration with dependsOn',
turbo: {
tasks: {
build: {
dependsOn: ['^build'],
},
},
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
targetDefaults: {
build: {
dependsOn: ['^build'],
cache: true,
},
},
},
},
{
description: 'task configuration with outputs',
turbo: {
tasks: {
build: {
outputs: ['dist/**', '.next/**'],
},
},
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
targetDefaults: {
build: {
outputs: ['{projectRoot}/dist/**', '{projectRoot}/.next/**'],
cache: true,
},
},
},
},
{
description: 'task configuration with inputs',
turbo: {
tasks: {
build: {
inputs: ['src/**/*.tsx', 'test/**/*.tsx'],
},
},
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
targetDefaults: {
build: {
inputs: [
'{projectRoot}/src/**/*.tsx',
'{projectRoot}/test/**/*.tsx',
],
cache: true,
},
},
},
},
{
description: 'cache configuration',
turbo: {
tasks: {
build: {
cache: true,
},
dev: {
cache: false,
},
},
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
targetDefaults: {
build: {
cache: true,
},
dev: {
cache: false,
},
},
},
},
{
description: 'cache directory configuration',
turbo: {
cacheDir: './node_modules/.cache/turbo',
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
cacheDirectory: '.nx/cache',
},
},
{
description: 'skip project-specific task configurations',
turbo: {
tasks: {
build: {
dependsOn: ['^build'],
},
'docs#build': {
dependsOn: ['^build'],
outputs: ['www/**'],
},
},
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
targetDefaults: {
build: {
dependsOn: ['^build'],
cache: true,
},
},
},
},
{
description: 'complex configuration combining multiple features',
turbo: {
globalDependencies: ['babel.config.json'],
globalEnv: ['NODE_ENV'],
cacheDir: './node_modules/.cache/turbo',
tasks: {
build: {
dependsOn: ['^build'],
outputs: ['dist/**'],
inputs: ['src/**/*'],
cache: true,
},
test: {
dependsOn: ['build'],
outputs: ['coverage/**'],
cache: true,
},
dev: {
cache: false,
},
},
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
namedInputs: {
sharedGlobals: [
'{workspaceRoot}/babel.config.json',
{ env: 'NODE_ENV' },
],
default: ['{projectRoot}/**/*', 'sharedGlobals'],
},
cacheDirectory: '.nx/cache',
targetDefaults: {
build: {
dependsOn: ['^build'],
outputs: ['{projectRoot}/dist/**'],
inputs: ['{projectRoot}/src/**/*'],
cache: true,
},
test: {
dependsOn: ['build'],
outputs: ['{projectRoot}/coverage/**'],
cache: true,
},
dev: {
cache: false,
},
},
},
},
{
description: 'turbo starter with $TURBO_DEFAULT$',
turbo: {
$schema: 'https://turbo.build/schema.json',
ui: 'tui',
tasks: {
build: {
dependsOn: ['^build'],
inputs: ['$TURBO_DEFAULT$', '.env*'],
outputs: ['.next/**', '!.next/cache/**'],
},
lint: {
dependsOn: ['^lint'],
},
'check-types': {
dependsOn: ['^check-types'],
},
dev: {
cache: false,
persistent: true,
},
},
},
nx: {
$schema: './node_modules/nx/schemas/nx-schema.json',
targetDefaults: {
build: {
dependsOn: ['^build'],
inputs: ['{projectRoot}/**/*', '{projectRoot}/.env*'],
outputs: [
'{projectRoot}/.next/**',
'!{projectRoot}/.next/cache/**',
],
cache: true,
},
lint: {
dependsOn: ['^lint'],
cache: true,
},
'check-types': {
dependsOn: ['^check-types'],
cache: true,
},
dev: {
cache: false,
},
},
},
},
])('$description', ({ turbo, nx }) => {
expect(createNxJsonFromTurboJson(turbo)).toEqual(nx);
});
});
});

View File

@ -19,7 +19,7 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
import { printSuccessMessage } from '../../../nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud';
import { repoUsesGithub } from '../../../nx-cloud/utilities/url-shorten';
import { connectWorkspaceToCloud } from '../../connect/connect-to-nx-cloud';
import { deduceDefaultBase as gitInitDefaultBase } from '../../../utils/default-base';
import { deduceDefaultBase } from './deduce-default-base';
export function createNxJsonFile(
repoRoot: string,
@ -61,52 +61,127 @@ export function createNxJsonFile(
delete nxJson.targetDefaults;
}
nxJson.defaultBase ??= deduceDefaultBase();
const defaultBase = deduceDefaultBase();
// Do not add defaultBase if it is inferred to be the Nx default value of main
if (defaultBase !== 'main') {
nxJson.defaultBase ??= defaultBase;
}
writeJsonFile(nxJsonPath, nxJson);
}
function deduceDefaultBase() {
try {
execSync(`git rev-parse --verify main`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'main';
} catch {
try {
execSync(`git rev-parse --verify dev`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'dev';
} catch {
try {
execSync(`git rev-parse --verify develop`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'develop';
} catch {
try {
execSync(`git rev-parse --verify next`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'next';
} catch {
try {
execSync(`git rev-parse --verify master`, {
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: false,
});
return 'master';
} catch {
return gitInitDefaultBase();
}
}
}
export function createNxJsonFromTurboJson(
turboJson: Record<string, any>
): NxJsonConfiguration {
const nxJson: NxJsonConfiguration = {
$schema: './node_modules/nx/schemas/nx-schema.json',
};
// Handle global dependencies
if (turboJson.globalDependencies?.length > 0) {
nxJson.namedInputs = {
sharedGlobals: turboJson.globalDependencies.map(
(dep) => `{workspaceRoot}/${dep}`
),
default: ['{projectRoot}/**/*', 'sharedGlobals'],
};
}
// Handle global env vars
if (turboJson.globalEnv?.length > 0) {
nxJson.namedInputs = nxJson.namedInputs || {};
nxJson.namedInputs.sharedGlobals = nxJson.namedInputs.sharedGlobals || [];
nxJson.namedInputs.sharedGlobals.push(
...turboJson.globalEnv.map((env) => ({ env }))
);
nxJson.namedInputs.default = nxJson.namedInputs.default || [];
if (!nxJson.namedInputs.default.includes('{projectRoot}/**/*')) {
nxJson.namedInputs.default.push('{projectRoot}/**/*');
}
if (!nxJson.namedInputs.default.includes('sharedGlobals')) {
nxJson.namedInputs.default.push('sharedGlobals');
}
}
// Handle task configurations
if (turboJson.tasks) {
nxJson.targetDefaults = {};
for (const [taskName, taskConfig] of Object.entries(turboJson.tasks)) {
// Skip project-specific tasks (containing #)
if (taskName.includes('#')) continue;
const config = taskConfig as any;
nxJson.targetDefaults[taskName] = {};
// Handle dependsOn
if (config.dependsOn?.length > 0) {
nxJson.targetDefaults[taskName].dependsOn = config.dependsOn;
}
// Handle inputs
if (config.inputs?.length > 0) {
nxJson.targetDefaults[taskName].inputs = config.inputs
.map((input) => {
if (input === '$TURBO_DEFAULT$') {
return '{projectRoot}/**/*';
}
// Don't add projectRoot if it's already there or if it's an env var
if (
input.startsWith('{projectRoot}/') ||
input.startsWith('{env.') ||
input.startsWith('$')
)
return input;
return `{projectRoot}/${input}`;
})
.map((input) => {
// Don't add projectRoot if it's already there or if it's an env var
if (
input.startsWith('{projectRoot}/') ||
input.startsWith('{env.') ||
input.startsWith('$')
)
return input;
return `{projectRoot}/${input}`;
});
}
// Handle outputs
if (config.outputs?.length > 0) {
nxJson.targetDefaults[taskName].outputs = config.outputs.map(
(output) => {
// Don't add projectRoot if it's already there
if (output.startsWith('{projectRoot}/')) return output;
// Handle negated patterns by adding projectRoot after the !
if (output.startsWith('!')) {
return `!{projectRoot}/${output.slice(1)}`;
}
return `{projectRoot}/${output}`;
}
);
}
// Handle cache setting - true by default in Turbo
nxJson.targetDefaults[taskName].cache = config.cache !== false;
}
}
/**
* The fact that cacheDir was in use suggests the user had a reason for deviating from the default.
* We can't know what that reason was, nor if it would still be applicable in Nx, but we can at least
* improve discoverability of the relevant Nx option by explicitly including it with its default value.
*/
if (turboJson.cacheDir) {
nxJson.cacheDirectory = '.nx/cache';
}
const defaultBase = deduceDefaultBase();
// Do not add defaultBase if it is inferred to be the Nx default value of main
if (defaultBase !== 'main') {
nxJson.defaultBase ??= defaultBase;
}
return nxJson;
}
export function addDepsToPackageJson(
@ -168,6 +243,7 @@ export async function initCloud(
| 'nx-init-monorepo'
| 'nx-init-nest'
| 'nx-init-npm-repo'
| 'nx-init-turborepo'
) {
const token = await connectWorkspaceToCloud({
installationSource,

View File

@ -1,13 +1,26 @@
import { existsSync } from 'fs';
import { PackageJson } from '../../utils/package-json';
import { prompt } from 'enquirer';
import { prerelease } from 'semver';
import { output } from '../../utils/output';
import { getPackageManagerCommand } from '../../utils/package-manager';
import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts';
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
import { runNxSync } from '../../utils/child-process';
import { readJsonFile } from '../../utils/fileutils';
import { getPackageNameFromImportPath } from '../../utils/get-package-name-from-import-path';
import { output } from '../../utils/output';
import { PackageJson } from '../../utils/package-json';
import { getPackageManagerCommand } from '../../utils/package-manager';
import { nxVersion } from '../../utils/versions';
import { globWithWorkspaceContextSync } from '../../utils/workspace-context';
import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud';
import {
configurePlugins,
runPackageManagerInstallPlugins,
} from './configure-plugins';
import { addNxToMonorepo } from './implementation/add-nx-to-monorepo';
import { addNxToNpmRepo } from './implementation/add-nx-to-npm-repo';
import { addNxToTurborepo } from './implementation/add-nx-to-turborepo';
import { addNxToAngularCliRepo } from './implementation/angular';
import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts';
import {
createNxJsonFile,
initCloud,
@ -15,18 +28,6 @@ import {
printFinalMessage,
updateGitIgnore,
} from './implementation/utils';
import { prompt } from 'enquirer';
import { addNxToAngularCliRepo } from './implementation/angular';
import { globWithWorkspaceContextSync } from '../../utils/workspace-context';
import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud';
import { addNxToNpmRepo } from './implementation/add-nx-to-npm-repo';
import { addNxToMonorepo } from './implementation/add-nx-to-monorepo';
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
import { getPackageNameFromImportPath } from '../../utils/get-package-name-from-import-path';
import {
configurePlugins,
runPackageManagerInstallPlugins,
} from './configure-plugins';
export interface InitArgs {
interactive: boolean;
@ -82,7 +83,32 @@ export async function initHandler(options: InitArgs): Promise<void> {
}
const packageJson: PackageJson = readJsonFile('package.json');
if (isMonorepo(packageJson)) {
const _isTurborepo = existsSync('turbo.json');
const _isMonorepo = isMonorepo(packageJson);
const learnMoreLink = _isTurborepo
? 'https://nx.dev/recipes/adopting-nx/from-turborepo'
: _isMonorepo
? 'https://nx.dev/getting-started/tutorials/npm-workspaces-tutorial'
: 'https://nx.dev/recipes/adopting-nx/adding-to-existing-project';
/**
* Turborepo users must have set up individual scripts already, and we keep the transition as minimal as possible.
* We log a message during the conversion process in addNxToTurborepo about how they can learn more about the power
* of Nx plugins and how it would allow them to infer all the relevant scripts automatically, including all cache
* inputs and outputs.
*/
if (_isTurborepo) {
await addNxToTurborepo({
interactive: options.interactive,
});
printFinalMessage({
learnMoreLink,
});
return;
}
if (_isMonorepo) {
await addNxToMonorepo({
interactive: options.interactive,
nxCloud: false,
@ -93,9 +119,7 @@ export async function initHandler(options: InitArgs): Promise<void> {
nxCloud: false,
});
}
const learnMoreLink = isMonorepo(packageJson)
? 'https://nx.dev/getting-started/tutorials/npm-workspaces-tutorial'
: 'https://nx.dev/recipes/adopting-nx/adding-to-existing-project';
const useNxCloud =
options.nxCloud ??
(options.interactive ? await connectExistingRepoToNxCloudPrompt() : false);

View File

@ -361,6 +361,7 @@ export interface NxSyncConfiguration {
* @note: when adding properties here add them to `allowedWorkspaceExtensions` in adapter/compat.ts
*/
export interface NxJsonConfiguration<T = '*' | string[]> {
$schema?: string;
/**
* Optional (additional) Nx.json configuration file which becomes a base for this one
*/