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

View File

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

View File

@ -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: [

View File

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

View File

@ -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

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