feat(misc): nx init should work on non-monorepo projects

This commit is contained in:
Victor Savkin 2022-11-16 13:03:47 -05:00
parent 90f2791303
commit 661bea436a
7 changed files with 357 additions and 165 deletions

View File

@ -2,6 +2,7 @@ import {
cleanupProject, cleanupProject,
createNonNxProjectDirectory, createNonNxProjectDirectory,
getPackageManagerCommand, getPackageManagerCommand,
getPublishedVersion,
getSelectedPackageManager, getSelectedPackageManager,
renameFile, renameFile,
runCLI, runCLI,
@ -16,8 +17,8 @@ describe('nx init', () => {
afterEach(() => cleanupProject()); afterEach(() => cleanupProject());
it('should work', () => { it('should work in a monorepo', () => {
createNonNxProjectDirectory(); createNonNxProjectDirectory('monorepo', true);
updateFile( updateFile(
'packages/package/package.json', 'packages/package/package.json',
JSON.stringify({ JSON.stringify({
@ -30,7 +31,9 @@ describe('nx init', () => {
runCommand(pmc.install); 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(output).toContain('Enabled computation caching');
expect(runCLI('run package:echo')).toContain('123'); expect(runCLI('run package:echo')).toContain('123');
@ -38,4 +41,32 @@ describe('nx init', () => {
expect(runCLI('run package:echo')).toContain('123'); 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');
});
}); });

View File

@ -136,14 +136,17 @@ export function readProjectConfig(projectName: string): ProjectConfiguration {
return readJson(path); return readJson(path);
} }
export function createNonNxProjectDirectory(name = uniq('proj')) { export function createNonNxProjectDirectory(
name = uniq('proj'),
addWorkspaces = true
) {
projName = name; projName = name;
ensureDirSync(tmpProjPath()); ensureDirSync(tmpProjPath());
createFile( createFile(
'package.json', 'package.json',
JSON.stringify({ JSON.stringify({
name, name,
workspaces: ['packages/*'], workspaces: addWorkspaces ? ['packages/*'] : undefined,
}) })
); );
} }

View File

@ -2,17 +2,20 @@
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import { execSync } from 'child_process';
import * as enquirer from 'enquirer'; import * as enquirer from 'enquirer';
import { joinPathFragments } from 'nx/src/utils/path'; import { joinPathFragments } from 'nx/src/utils/path';
import { import { getPackageManagerCommand } from 'nx/src/utils/package-manager';
getPackageManagerCommand,
PackageManagerCommands,
} from 'nx/src/utils/package-manager';
import { output } from 'nx/src/utils/output'; 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 ignore from 'ignore';
import * as yargsParser from 'yargs-parser'; import * as yargsParser from 'yargs-parser';
import {
askAboutNxCloud,
createNxJsonFile,
initCloud,
runInstall,
addDepsToPackageJson,
} from 'nx/src/nx-init/utils';
const parsedArgs = yargsParser(process.argv, { const parsedArgs = yargsParser(process.argv, {
boolean: ['yes'], boolean: ['yes'],
@ -38,13 +41,12 @@ async function addNxToMonorepo() {
output.log({ title: `🐳 Nx initialization` }); output.log({ title: `🐳 Nx initialization` });
const pmc = getPackageManagerCommand();
const packageJsonFiles = allProjectPackageJsonFiles(repoRoot); const packageJsonFiles = allProjectPackageJsonFiles(repoRoot);
const scripts = combineAllScriptNames(repoRoot, packageJsonFiles); const scripts = combineAllScriptNames(repoRoot, packageJsonFiles);
let targetDefaults: string[]; let targetDefaults: string[];
let cacheableOperations: string[]; let cacheableOperations: string[];
let scriptOutputs = {}; let scriptOutputs = {} as { [script: string]: string };
let useCloud: boolean; let useCloud: boolean;
if (parsedArgs.yes !== true) { if (parsedArgs.yes !== true) {
@ -57,8 +59,7 @@ async function addNxToMonorepo() {
{ {
type: 'multiselect', type: 'multiselect',
name: 'targetDefaults', name: 'targetDefaults',
message: message: `Which scripts need to be run in order? (e.g. before building a project, dependent projects must be built.)`,
'Which of the following scripts need to be run in deterministic/topological order?',
choices: scripts, choices: scripts,
}, },
])) as any ])) as any
@ -70,7 +71,7 @@ async function addNxToMonorepo() {
type: 'multiselect', type: 'multiselect',
name: 'cacheableOperations', name: 'cacheableOperations',
message: 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, choices: scripts,
}, },
])) as any ])) as any
@ -78,13 +79,15 @@ async function addNxToMonorepo() {
for (const scriptName of cacheableOperations) { for (const scriptName of cacheableOperations) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
scriptOutputs[scriptName] = await enquirer.prompt([ scriptOutputs[scriptName] = (
{ await enquirer.prompt([
type: 'input', {
name: scriptName, type: 'input',
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)`, 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(); useCloud = await askAboutNxCloud();
@ -98,42 +101,20 @@ async function addNxToMonorepo() {
repoRoot, repoRoot,
targetDefaults, targetDefaults,
cacheableOperations, cacheableOperations,
scriptOutputs scriptOutputs,
undefined
); );
addDepsToPackageJson(repoRoot, useCloud); addDepsToPackageJson(repoRoot, useCloud);
output.log({ title: `📦 Installing dependencies` }); output.log({ title: `📦 Installing dependencies` });
runInstall(repoRoot, pmc); runInstall(repoRoot);
if (useCloud) { if (useCloud) {
initCloud(repoRoot, pmc); initCloud(repoRoot);
} }
printFinalMessage(pmc); printFinalMessage();
}
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');
} }
// scanning package.json files // scanning package.json files
@ -198,102 +179,8 @@ function combineAllScriptNames(
return [...res]; return [...res];
} }
function createNxJsonFile( function printFinalMessage() {
repoRoot: string, const pmc = getPackageManagerCommand();
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) {
output.success({ output.success({
title: `🎉 Done!`, title: `🎉 Done!`,
bodyLines: [ bodyLines: [

View File

@ -1,15 +1,34 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { existsSync } from 'fs'; 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(' '); const args = process.argv.slice(2).join(' ');
if (existsSync('package.json')) { if (existsSync('package.json')) {
execSync(`npx --yes add-nx-to-monorepo@latest ${args}`, { if (isMonorepo()) {
stdio: [0, 1, 2], // TODO: vsavkin remove add-nx-to-monorepo
}); execSync(`npx --yes add-nx-to-monorepo@latest ${args}`, {
stdio: [0, 1, 2],
});
} else {
await addNxToNpmRepo();
}
} else { } else {
execSync(`npx --yes create-nx-workspace@latest ${args}`, { execSync(`npx --yes create-nx-workspace@latest ${args}`, {
stdio: [0, 1, 2], 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;
}

View File

@ -161,21 +161,21 @@ export function calculateDefaultProjectName(
workspaceConfiguration: ProjectsConfigurations & NxJsonConfiguration workspaceConfiguration: ProjectsConfigurations & NxJsonConfiguration
) { ) {
let relativeCwd = cwd.replace(/\\/g, '/').split(root.replace(/\\/g, '/'))[1]; let relativeCwd = cwd.replace(/\\/g, '/').split(root.replace(/\\/g, '/'))[1];
if (relativeCwd) {
relativeCwd = relativeCwd.startsWith('/') relativeCwd = relativeCwd.startsWith('/')
? relativeCwd.substring(1) ? relativeCwd.substring(1)
: relativeCwd; : relativeCwd;
const matchingProject = Object.keys(workspaceConfiguration.projects).find( const matchingProject = Object.keys(workspaceConfiguration.projects).find(
(p) => { (p) => {
const projectRoot = workspaceConfiguration.projects[p].root; const projectRoot = workspaceConfiguration.projects[p].root;
return ( return (
relativeCwd == projectRoot || relativeCwd == projectRoot ||
relativeCwd.startsWith(`${projectRoot}/`) (relativeCwd == '' && projectRoot == '.') ||
); relativeCwd.startsWith(`${projectRoot}/`)
} );
); }
if (matchingProject) return matchingProject; );
} if (matchingProject) return matchingProject;
return ( return (
(workspaceConfiguration.cli as { defaultProjectName: string }) (workspaceConfiguration.cli as { defaultProjectName: string })
?.defaultProjectName || workspaceConfiguration.defaultProject ?.defaultProjectName || workspaceConfiguration.defaultProject

View File

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

View File

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