diff --git a/e2e/nx-init/src/nx-init.test.ts b/e2e/nx-init/src/nx-init.test.ts index 376c04c057ab8..b9cad80eb927d 100644 --- a/e2e/nx-init/src/nx-init.test.ts +++ b/e2e/nx-init/src/nx-init.test.ts @@ -2,6 +2,7 @@ import { cleanupProject, createNonNxProjectDirectory, getPackageManagerCommand, + getPublishedVersion, getSelectedPackageManager, renameFile, runCLI, @@ -16,8 +17,8 @@ describe('nx init', () => { afterEach(() => cleanupProject()); - it('should work', () => { - createNonNxProjectDirectory(); + it('should work in a monorepo', () => { + createNonNxProjectDirectory('monorepo', true); updateFile( 'packages/package/package.json', JSON.stringify({ @@ -30,7 +31,9 @@ describe('nx init', () => { runCommand(pmc.install); - const output = runCommand(`${pmc.runUninstalledPackage} nx init -y`); + const output = runCommand( + `${pmc.runUninstalledPackage} nx@${getPublishedVersion()} init -y` + ); expect(output).toContain('Enabled computation caching'); expect(runCLI('run package:echo')).toContain('123'); @@ -38,4 +41,32 @@ describe('nx init', () => { expect(runCLI('run package:echo')).toContain('123'); }); + + it('should work in a regular npm repo ttt', () => { + createNonNxProjectDirectory('regular-repo', false); + updateFile( + 'package.json', + JSON.stringify({ + name: 'package', + scripts: { + echo: 'echo 123', + }, + }) + ); + + runCommand(pmc.install); + + const output = runCommand( + `${ + pmc.runUninstalledPackage + } nx@${getPublishedVersion()} init -y --cacheable=echo` + ); + console.log(output); + expect(output).toContain('Enabled computation caching'); + + expect(runCLI('echo')).toContain('123'); + renameFile('nx.json', 'nx.json.old'); + + expect(runCLI('echo')).toContain('123'); + }); }); diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts index bbe49987b82ef..42b33efc1813b 100644 --- a/e2e/utils/index.ts +++ b/e2e/utils/index.ts @@ -136,14 +136,17 @@ export function readProjectConfig(projectName: string): ProjectConfiguration { return readJson(path); } -export function createNonNxProjectDirectory(name = uniq('proj')) { +export function createNonNxProjectDirectory( + name = uniq('proj'), + addWorkspaces = true +) { projName = name; ensureDirSync(tmpProjPath()); createFile( 'package.json', JSON.stringify({ name, - workspaces: ['packages/*'], + workspaces: addWorkspaces ? ['packages/*'] : undefined, }) ); } diff --git a/packages/add-nx-to-monorepo/src/add-nx-to-monorepo.ts b/packages/add-nx-to-monorepo/src/add-nx-to-monorepo.ts index 84aa7c31e4c0d..92676814246d7 100644 --- a/packages/add-nx-to-monorepo/src/add-nx-to-monorepo.ts +++ b/packages/add-nx-to-monorepo/src/add-nx-to-monorepo.ts @@ -2,17 +2,20 @@ import * as path from 'path'; import * as fs from 'fs'; -import { execSync } from 'child_process'; import * as enquirer from 'enquirer'; import { joinPathFragments } from 'nx/src/utils/path'; -import { - getPackageManagerCommand, - PackageManagerCommands, -} from 'nx/src/utils/package-manager'; +import { getPackageManagerCommand } from 'nx/src/utils/package-manager'; import { output } from 'nx/src/utils/output'; -import { readJsonFile, writeJsonFile } from 'nx/src/utils/fileutils'; +import { readJsonFile } from 'nx/src/utils/fileutils'; import ignore from 'ignore'; import * as yargsParser from 'yargs-parser'; +import { + askAboutNxCloud, + createNxJsonFile, + initCloud, + runInstall, + addDepsToPackageJson, +} from 'nx/src/nx-init/utils'; const parsedArgs = yargsParser(process.argv, { boolean: ['yes'], @@ -38,13 +41,12 @@ async function addNxToMonorepo() { output.log({ title: `🐳 Nx initialization` }); - const pmc = getPackageManagerCommand(); const packageJsonFiles = allProjectPackageJsonFiles(repoRoot); const scripts = combineAllScriptNames(repoRoot, packageJsonFiles); let targetDefaults: string[]; let cacheableOperations: string[]; - let scriptOutputs = {}; + let scriptOutputs = {} as { [script: string]: string }; let useCloud: boolean; if (parsedArgs.yes !== true) { @@ -57,8 +59,7 @@ async function addNxToMonorepo() { { type: 'multiselect', name: 'targetDefaults', - message: - 'Which of the following scripts need to be run in deterministic/topological order?', + message: `Which scripts need to be run in order? (e.g. before building a project, dependent projects must be built.)`, choices: scripts, }, ])) as any @@ -70,7 +71,7 @@ async function addNxToMonorepo() { type: 'multiselect', name: 'cacheableOperations', message: - 'Which of the following scripts are cacheable? (Produce the same output given the same input, e.g. build, test and lint usually are, serve and start are not)', + 'Which scripts are cacheable? (Produce the same output given the same input, e.g. build, test and lint usually are, serve and start are not)', choices: scripts, }, ])) as any @@ -78,13 +79,15 @@ async function addNxToMonorepo() { for (const scriptName of cacheableOperations) { // eslint-disable-next-line no-await-in-loop - scriptOutputs[scriptName] = await enquirer.prompt([ - { - type: 'input', - name: scriptName, - message: `Does the "${scriptName}" script create any outputs? If not, leave blank, otherwise provide a path relative to a project root (e.g. dist, lib, build, coverage)`, - }, - ]); + scriptOutputs[scriptName] = ( + await enquirer.prompt([ + { + type: 'input', + name: scriptName, + message: `Does the "${scriptName}" script create any outputs? If not, leave blank, otherwise provide a path relative to a project root (e.g. dist, lib, build, coverage)`, + }, + ]) + )[scriptName]; } useCloud = await askAboutNxCloud(); @@ -98,42 +101,20 @@ async function addNxToMonorepo() { repoRoot, targetDefaults, cacheableOperations, - scriptOutputs + scriptOutputs, + undefined ); addDepsToPackageJson(repoRoot, useCloud); output.log({ title: `📦 Installing dependencies` }); - runInstall(repoRoot, pmc); + runInstall(repoRoot); if (useCloud) { - initCloud(repoRoot, pmc); + initCloud(repoRoot); } - printFinalMessage(pmc); -} - -function askAboutNxCloud() { - return enquirer - .prompt([ - { - name: 'NxCloud', - message: `Enable distributed caching to make your CI faster`, - type: 'autocomplete', - choices: [ - { - name: 'Yes', - hint: 'I want faster builds', - }, - - { - name: 'No', - }, - ], - initial: 'Yes' as any, - }, - ]) - .then((a: { NxCloud: 'Yes' | 'No' }) => a.NxCloud === 'Yes'); + printFinalMessage(); } // scanning package.json files @@ -198,102 +179,8 @@ function combineAllScriptNames( return [...res]; } -function createNxJsonFile( - repoRoot: string, - targetDefaults: string[], - cacheableOperations: string[], - scriptOutputs: { [name: string]: string } -) { - const nxJsonPath = joinPathFragments(repoRoot, 'nx.json'); - let nxJson = {} as any; - try { - nxJson = readJsonFile(nxJsonPath); - // eslint-disable-next-line no-empty - } catch {} - - nxJson.tasksRunnerOptions ||= {}; - nxJson.tasksRunnerOptions.default ||= {}; - nxJson.tasksRunnerOptions.default.runner ||= 'nx/tasks-runners/default'; - nxJson.tasksRunnerOptions.default.options ||= {}; - nxJson.tasksRunnerOptions.default.options.cacheableOperations = - cacheableOperations; - nxJson.targetDefaults ||= {}; - for (const scriptName of targetDefaults) { - nxJson.targetDefaults[scriptName] ||= {}; - nxJson.targetDefaults[scriptName] = { dependsOn: [`^${scriptName}`] }; - } - for (const [scriptName, scriptAnswerData] of Object.entries(scriptOutputs)) { - if (!scriptAnswerData[scriptName]) { - // eslint-disable-next-line no-continue - continue; - } - nxJson.targetDefaults[scriptName] ||= {}; - nxJson.targetDefaults[scriptName].outputs = [ - `{projectRoot}/${scriptAnswerData[scriptName]}`, - ]; - } - nxJson.defaultBase = deduceDefaultBase(); - writeJsonFile(nxJsonPath, nxJson); -} - -function deduceDefaultBase() { - try { - execSync(`git rev-parse --verify main`, { - stdio: ['ignore', 'ignore', 'ignore'], - }); - return 'main'; - } catch { - try { - execSync(`git rev-parse --verify dev`, { - stdio: ['ignore', 'ignore', 'ignore'], - }); - return 'dev'; - } catch { - try { - execSync(`git rev-parse --verify develop`, { - stdio: ['ignore', 'ignore', 'ignore'], - }); - return 'develop'; - } catch { - try { - execSync(`git rev-parse --verify next`, { - stdio: ['ignore', 'ignore', 'ignore'], - }); - return 'next'; - } catch { - return 'master'; - } - } - } - } -} - -// add dependencies -function addDepsToPackageJson(repoRoot: string, useCloud: boolean) { - const json = readJsonFile(joinPathFragments(repoRoot, `package.json`)); - if (!json.devDependencies) json.devDependencies = {}; - json.devDependencies['nx'] = require('../package.json').version; - if (useCloud) { - json.devDependencies['@nrwl/nx-cloud'] = 'latest'; - } - writeJsonFile(`package.json`, json); -} - -function runInstall(repoRoot: string, pmc: PackageManagerCommands) { - execSync(pmc.install, { stdio: [0, 1, 2], cwd: repoRoot }); -} - -function initCloud(repoRoot: string, pmc: PackageManagerCommands) { - execSync( - `${pmc.exec} nx g @nrwl/nx-cloud:init --installationSource=add-nx-to-monorepo`, - { - stdio: [0, 1, 2], - cwd: repoRoot, - } - ); -} - -function printFinalMessage(pmc: PackageManagerCommands) { +function printFinalMessage() { + const pmc = getPackageManagerCommand(); output.success({ title: `🎉 Done!`, bodyLines: [ diff --git a/packages/nx/src/command-line/init.ts b/packages/nx/src/command-line/init.ts index 052f1897fe7c2..5a9a27e0d3301 100644 --- a/packages/nx/src/command-line/init.ts +++ b/packages/nx/src/command-line/init.ts @@ -1,15 +1,34 @@ import { execSync } from 'child_process'; import { existsSync } from 'fs'; +import { readJsonFile } from '../utils/fileutils'; +import { addNxToNpmRepo } from '../nx-init/add-nx-to-npm-repo'; -export function initHandler() { +export async function initHandler() { const args = process.argv.slice(2).join(' '); if (existsSync('package.json')) { - execSync(`npx --yes add-nx-to-monorepo@latest ${args}`, { - stdio: [0, 1, 2], - }); + if (isMonorepo()) { + // TODO: vsavkin remove add-nx-to-monorepo + execSync(`npx --yes add-nx-to-monorepo@latest ${args}`, { + stdio: [0, 1, 2], + }); + } else { + await addNxToNpmRepo(); + } } else { execSync(`npx --yes create-nx-workspace@latest ${args}`, { stdio: [0, 1, 2], }); } } + +function isMonorepo() { + const packageJson = readJsonFile('package.json'); + if (!!packageJson.workspaces) return true; + + if (existsSync('pnpm-workspace.yaml') || existsSync('pnpm-workspace.yml')) + return true; + + if (existsSync('lerna.json')) return true; + + return false; +} diff --git a/packages/nx/src/command-line/run-one.ts b/packages/nx/src/command-line/run-one.ts index ebe2a7bc3897f..84413d941adcb 100644 --- a/packages/nx/src/command-line/run-one.ts +++ b/packages/nx/src/command-line/run-one.ts @@ -161,21 +161,21 @@ export function calculateDefaultProjectName( workspaceConfiguration: ProjectsConfigurations & NxJsonConfiguration ) { let relativeCwd = cwd.replace(/\\/g, '/').split(root.replace(/\\/g, '/'))[1]; - if (relativeCwd) { - relativeCwd = relativeCwd.startsWith('/') - ? relativeCwd.substring(1) - : relativeCwd; - const matchingProject = Object.keys(workspaceConfiguration.projects).find( - (p) => { - const projectRoot = workspaceConfiguration.projects[p].root; - return ( - relativeCwd == projectRoot || - relativeCwd.startsWith(`${projectRoot}/`) - ); - } - ); - if (matchingProject) return matchingProject; - } + + relativeCwd = relativeCwd.startsWith('/') + ? relativeCwd.substring(1) + : relativeCwd; + const matchingProject = Object.keys(workspaceConfiguration.projects).find( + (p) => { + const projectRoot = workspaceConfiguration.projects[p].root; + return ( + relativeCwd == projectRoot || + (relativeCwd == '' && projectRoot == '.') || + relativeCwd.startsWith(`${projectRoot}/`) + ); + } + ); + if (matchingProject) return matchingProject; return ( (workspaceConfiguration.cli as { defaultProjectName: string }) ?.defaultProjectName || workspaceConfiguration.defaultProject diff --git a/packages/nx/src/nx-init/add-nx-to-npm-repo.ts b/packages/nx/src/nx-init/add-nx-to-npm-repo.ts new file mode 100644 index 0000000000000..28e2e40a8420b --- /dev/null +++ b/packages/nx/src/nx-init/add-nx-to-npm-repo.ts @@ -0,0 +1,122 @@ +import { output } from '../utils/output'; +import { getPackageManagerCommand } from '../utils/package-manager'; +import * as yargsParser from 'yargs-parser'; +import * as enquirer from 'enquirer'; +import { readJsonFile, writeJsonFile } from '../utils/fileutils'; +import { + addDepsToPackageJson, + askAboutNxCloud, + createNxJsonFile, + initCloud, + runInstall, +} from './utils'; +import { joinPathFragments } from 'nx/src/utils/path'; + +const parsedArgs = yargsParser(process.argv, { + boolean: ['yes'], + string: ['cacheable'], // only used for testing + alias: { + yes: ['y'], + }, +}); + +export async function addNxToNpmRepo() { + const repoRoot = process.cwd(); + + output.log({ title: `🐳 Nx initialization` }); + + let cacheableOperations: string[]; + let scriptOutputs = {}; + let useCloud: boolean; + + const packageJson = readJsonFile('package.json'); + const scripts = Object.keys(packageJson.scripts).filter( + (s) => !s.startsWith('pre') && !s.startsWith('post') + ); + + if (parsedArgs.yes !== true) { + output.log({ + title: `🧑‍🔧 Please answer the following questions about the scripts found in your package.json in order to generate task runner configuration`, + }); + + cacheableOperations = ( + (await enquirer.prompt([ + { + type: 'multiselect', + name: 'cacheableOperations', + message: + 'Which of the following scripts are cacheable? (Produce the same output given the same input, e.g. build, test and lint usually are, serve and start are not)', + choices: scripts, + }, + ])) as any + ).cacheableOperations; + + for (const scriptName of cacheableOperations) { + // eslint-disable-next-line no-await-in-loop + scriptOutputs[scriptName] = ( + await enquirer.prompt([ + { + type: 'input', + name: scriptName, + message: `Does the "${scriptName}" script create any outputs? If not, leave blank, otherwise provide a path (e.g. dist, lib, build, coverage)`, + }, + ]) + )[scriptName]; + } + + useCloud = await askAboutNxCloud(); + } else { + cacheableOperations = parsedArgs.cacheable + ? parsedArgs.cacheable.split(',') + : []; + useCloud = false; + } + + createNxJsonFile(repoRoot, [], cacheableOperations, {}, packageJson.name); + + addDepsToPackageJson(repoRoot, useCloud); + markRootPackageJsonAsNxProject(repoRoot, cacheableOperations, scriptOutputs); + + output.log({ title: `📦 Installing dependencies` }); + + runInstall(repoRoot); + + if (useCloud) { + initCloud(repoRoot); + } + + printFinalMessage(); +} + +function printFinalMessage() { + output.success({ + title: `🎉 Done!`, + bodyLines: [ + `- Enabled computation caching!`, + `- Learn more at https://nx.dev/recipes/adopting-nx/adding-to-monorepo`, + ], + }); +} + +export function markRootPackageJsonAsNxProject( + repoRoot: string, + cacheableScripts: string[], + scriptOutputs: { [script: string]: string } +) { + const json = readJsonFile(joinPathFragments(repoRoot, `package.json`)); + json.nx = { includeScripts: cacheableScripts }; + for (let script of Object.keys(scriptOutputs)) { + if (scriptOutputs[script]) { + json.nx.targets ||= {}; + json.nx.targets[script] = { + outputs: [`{projectRoot}/${scriptOutputs[script]}`], + }; + } + } + for (let script of cacheableScripts) { + if (json.scripts[script]) { + json.scripts[script] = `nx exec -- ${json.scripts[script]}`; + } + } + writeJsonFile(`package.json`, json); +} diff --git a/packages/nx/src/nx-init/utils.ts b/packages/nx/src/nx-init/utils.ts new file mode 100644 index 0000000000000..b8fd988a37be5 --- /dev/null +++ b/packages/nx/src/nx-init/utils.ts @@ -0,0 +1,130 @@ +import { joinPathFragments } from '../utils/path'; +import { readJsonFile, writeJsonFile } from '../utils/fileutils'; +import * as enquirer from 'enquirer'; +import { execSync } from 'child_process'; +import { getPackageManagerCommand } from '../utils/package-manager'; + +export function askAboutNxCloud() { + return enquirer + .prompt([ + { + name: 'NxCloud', + message: `Enable distributed caching to make your CI faster`, + type: 'autocomplete', + choices: [ + { + name: 'Yes', + hint: 'I want faster builds', + }, + + { + name: 'No', + }, + ], + initial: 'Yes' as any, + }, + ]) + .then((a: { NxCloud: 'Yes' | 'No' }) => a.NxCloud === 'Yes'); +} + +export function createNxJsonFile( + repoRoot: string, + targetDefaults: string[], + cacheableOperations: string[], + scriptOutputs: { [name: string]: string }, + defaultProject: string | undefined +) { + const nxJsonPath = joinPathFragments(repoRoot, 'nx.json'); + let nxJson = {} as any; + try { + nxJson = readJsonFile(nxJsonPath); + // eslint-disable-next-line no-empty + } catch {} + + nxJson.tasksRunnerOptions ||= {}; + nxJson.tasksRunnerOptions.default ||= {}; + nxJson.tasksRunnerOptions.default.runner ||= 'nx/tasks-runners/default'; + nxJson.tasksRunnerOptions.default.options ||= {}; + nxJson.tasksRunnerOptions.default.options.cacheableOperations = + cacheableOperations; + + if (targetDefaults.length > 0) { + nxJson.targetDefaults ||= {}; + for (const scriptName of targetDefaults) { + nxJson.targetDefaults[scriptName] ||= {}; + nxJson.targetDefaults[scriptName] = { dependsOn: [`^${scriptName}`] }; + } + for (const [scriptName, output] of Object.entries(scriptOutputs)) { + if (!output) { + // eslint-disable-next-line no-continue + continue; + } + nxJson.targetDefaults[scriptName] ||= {}; + nxJson.targetDefaults[scriptName].outputs = [`{projectRoot}/${output}`]; + } + } + nxJson.defaultBase = deduceDefaultBase(); + if (defaultProject) { + nxJson.defaultProject = defaultProject; + } + writeJsonFile(nxJsonPath, nxJson); +} + +function deduceDefaultBase() { + try { + execSync(`git rev-parse --verify main`, { + stdio: ['ignore', 'ignore', 'ignore'], + }); + return 'main'; + } catch { + try { + execSync(`git rev-parse --verify dev`, { + stdio: ['ignore', 'ignore', 'ignore'], + }); + return 'dev'; + } catch { + try { + execSync(`git rev-parse --verify develop`, { + stdio: ['ignore', 'ignore', 'ignore'], + }); + return 'develop'; + } catch { + try { + execSync(`git rev-parse --verify next`, { + stdio: ['ignore', 'ignore', 'ignore'], + }); + return 'next'; + } catch { + return 'master'; + } + } + } + } +} + +export function addDepsToPackageJson(repoRoot: string, useCloud: boolean) { + const path = joinPathFragments(repoRoot, `package.json`); + const json = readJsonFile(path); + if (!json.devDependencies) json.devDependencies = {}; + json.devDependencies['nx'] = require('../../package.json').version; + if (useCloud) { + json.devDependencies['@nrwl/nx-cloud'] = 'latest'; + } + writeJsonFile(path, json); +} + +export function runInstall(repoRoot: string) { + const pmc = getPackageManagerCommand(); + execSync(pmc.install, { stdio: [0, 1, 2], cwd: repoRoot }); +} + +export function initCloud(repoRoot: string) { + const pmc = getPackageManagerCommand(); + execSync( + `${pmc.exec} nx g @nrwl/nx-cloud:init --installationSource=add-nx-to-monorepo`, + { + stdio: [0, 1, 2], + cwd: repoRoot, + } + ); +}