nrwl--nx
770 行
23 KiB
TypeScript
770 行
23 KiB
TypeScript
import { existsSync } from 'fs';
|
|
import { basename } from 'path';
|
|
|
|
import { prompt } from 'enquirer';
|
|
import { prerelease } from 'semver';
|
|
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
|
|
import { readJsonFile, writeJsonFile } 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 '../nx-cloud/connect/connect-to-nx-cloud';
|
|
import { configurePlugins, installPluginPackages } from './configure-plugins';
|
|
import { determineAiAgents } from './ai-agent-prompts';
|
|
import { setupAiAgentsGenerator } from '../../ai/set-up-ai-agents/set-up-ai-agents';
|
|
import { FsTree, flushChanges } from '../../generators/tree';
|
|
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,
|
|
extractErrorName,
|
|
initCloud,
|
|
isMonorepo,
|
|
printFinalMessage,
|
|
readErrorStderr,
|
|
setNeverConnectToCloud,
|
|
toErrorString,
|
|
updateGitIgnore,
|
|
} from './implementation/utils';
|
|
import { ensurePackageHasProvenance } from '../../utils/provenance';
|
|
import { installPackageToTmp } from '../../devkit-internals';
|
|
import { handleImport } from '../../utils/handle-import';
|
|
import { isAiAgent } from '../../native';
|
|
import { Agent } from '../../ai/utils';
|
|
import { detectAiAgent } from '../../ai/detect-ai-agent';
|
|
import { MessageOptionKey, recordStat } from '../../utils/ab-testing';
|
|
import { ensureAnalyticsPreferenceSet } from '../../utils/analytics-prompt';
|
|
import { isCI } from '../../utils/is-ci';
|
|
import { detectPackageManager } from '../../utils/package-manager';
|
|
import {
|
|
logProgress,
|
|
writeAiOutput,
|
|
buildNeedsInputResult,
|
|
buildSuccessResult,
|
|
buildErrorResult,
|
|
writeErrorLog,
|
|
determineErrorCode,
|
|
DetectedPlugin,
|
|
} from './utils/ai-output';
|
|
|
|
export interface InitArgs {
|
|
interactive: boolean;
|
|
nxCloud?: boolean;
|
|
useDotNxInstallation?: boolean;
|
|
integrated?: boolean; // For Angular projects only
|
|
verbose?: boolean;
|
|
aiAgents?: Agent[];
|
|
plugins?: string; // 'skip' | 'all' | comma-separated list
|
|
cacheable?: string[]; // Cacheable operations (e.g., ['build', 'test', 'lint'])
|
|
}
|
|
|
|
export async function initHandler(
|
|
options: InitArgs,
|
|
inner = false
|
|
): Promise<void> {
|
|
// Use environment variable to force local execution
|
|
if (process.env.NX_USE_LOCAL === 'true' || inner) {
|
|
return await initHandlerImpl(options);
|
|
}
|
|
|
|
let cleanup: () => void | undefined;
|
|
try {
|
|
await ensurePackageHasProvenance('nx', 'latest');
|
|
const packageInstallResults = installPackageToTmp(
|
|
'nx',
|
|
'latest',
|
|
detectPackageManager(process.cwd())
|
|
);
|
|
cleanup = packageInstallResults.cleanup;
|
|
|
|
let modulePath = require.resolve('nx/src/command-line/init/init-v2.js', {
|
|
paths: [packageInstallResults.tempDir],
|
|
});
|
|
|
|
const module = await handleImport(modulePath);
|
|
const result = await module.initHandler(options, true);
|
|
cleanup();
|
|
return result;
|
|
} catch (error) {
|
|
if (cleanup) {
|
|
cleanup();
|
|
}
|
|
// Fall back to local implementation
|
|
return initHandlerImpl(options);
|
|
}
|
|
}
|
|
|
|
async function recordInitError(
|
|
error: unknown,
|
|
baseMeta: Record<string, string | boolean>
|
|
): Promise<void> {
|
|
const errorMessage = toErrorString(error);
|
|
const errorCode = determineErrorCode(error);
|
|
const stderr = readErrorStderr(error).trim();
|
|
const telemetryMessage = (
|
|
stderr ? `${errorMessage} | stderr: ${stderr.slice(-250)}` : errorMessage
|
|
).slice(0, 500);
|
|
const errorName = extractErrorName(error, stderr);
|
|
|
|
await recordStat({
|
|
command: 'init',
|
|
nxVersion,
|
|
useCloud: false,
|
|
meta: {
|
|
type: 'error',
|
|
errorCode,
|
|
errorName,
|
|
errorMessage: telemetryMessage,
|
|
...baseMeta,
|
|
},
|
|
});
|
|
|
|
if (baseMeta.aiAgent) {
|
|
const errorLogPath = writeErrorLog(error);
|
|
writeAiOutput(buildErrorResult(errorMessage, errorCode, errorLogPath));
|
|
} else {
|
|
// Restore the cursor in case the user bailed during an interactive
|
|
// prompt. Skip for AI agents — it would corrupt NDJSON output.
|
|
process.stdout.write('\x1b[?25h');
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
async function initHandlerImpl(options: InitArgs): Promise<void> {
|
|
process.env.NX_RUNNING_NX_INIT = 'true';
|
|
const baseMeta = {
|
|
nodeVersion: process.versions.node,
|
|
os: process.platform,
|
|
packageManager: detectPackageManager(),
|
|
aiAgent: isAiAgent(),
|
|
isCI: isCI(),
|
|
};
|
|
recordStat({
|
|
command: 'init',
|
|
nxVersion,
|
|
useCloud: false,
|
|
meta: { type: 'start', ...baseMeta },
|
|
});
|
|
|
|
try {
|
|
return await runInit(options, baseMeta);
|
|
} catch (error) {
|
|
await recordInitError(error, baseMeta);
|
|
}
|
|
}
|
|
|
|
async function runInit(
|
|
options: InitArgs,
|
|
baseMeta: Record<string, string | boolean>
|
|
): Promise<void> {
|
|
const version =
|
|
process.env.NX_VERSION ?? (prerelease(nxVersion) ? nxVersion : 'latest');
|
|
if (process.env.NX_VERSION) {
|
|
output.log({ title: `Using version ${process.env.NX_VERSION}` });
|
|
}
|
|
|
|
// AI agent mode: apply defaults for non-interactive operation
|
|
const aiMode = isAiAgent();
|
|
if (aiMode) {
|
|
options.interactive = false; // Force non-interactive
|
|
|
|
if (options.nxCloud === undefined) {
|
|
options.nxCloud = false; // Default to skip Nx Cloud
|
|
}
|
|
|
|
// Auto-detect .nx installation for non-JS projects
|
|
if (options.useDotNxInstallation === undefined) {
|
|
const hasPackageJson = existsSync('package.json');
|
|
options.useDotNxInstallation = !hasPackageJson;
|
|
}
|
|
|
|
// Auto-detect and set the current AI agent for setup
|
|
if (options.aiAgents === undefined) {
|
|
const detectedAgent = detectAiAgent();
|
|
if (detectedAgent) {
|
|
options.aiAgents = [detectedAgent];
|
|
}
|
|
}
|
|
|
|
// Set default cacheable operations for AI mode
|
|
// These are commonly cacheable scripts that benefit from caching
|
|
if (options.cacheable === undefined) {
|
|
options.cacheable = ['build', 'test', 'lint'];
|
|
}
|
|
|
|
logProgress('starting', 'Initializing Nx...');
|
|
}
|
|
|
|
// TODO(jack): Remove this Angular logic once `@nx/angular` is compatible with inferred targets.
|
|
if (existsSync('angular.json')) {
|
|
await addNxToAngularCliRepo({
|
|
...options,
|
|
integrated: !!options.integrated,
|
|
});
|
|
|
|
printFinalMessage({
|
|
learnMoreLink: 'https://nx.dev/technologies/angular/migration/angular',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// When in an empty directory (no package.json) and the user hasn't explicitly
|
|
// chosen a setup method, prompt them to pick between .nx and package.json setup.
|
|
// Skip the prompt when stdin is not a TTY (e.g. CI, e2e tests) to avoid hangs.
|
|
if (
|
|
!existsSync('package.json') &&
|
|
!options.useDotNxInstallation &&
|
|
options.interactive &&
|
|
!aiMode &&
|
|
process.stdin.isTTY
|
|
) {
|
|
const setupMode = await prompt<{ setupMode: string }>([
|
|
{
|
|
type: 'select',
|
|
name: 'setupMode',
|
|
message: 'How would you like to set up Nx in this directory?',
|
|
choices: [
|
|
{
|
|
name: '.nx installation (recommended for non-JavaScript projects)',
|
|
},
|
|
{
|
|
name: 'package.json installation (recommended for JavaScript/TypeScript projects)',
|
|
},
|
|
],
|
|
},
|
|
]).then((r) => r.setupMode);
|
|
|
|
if (setupMode.startsWith('package.json')) {
|
|
// Create a minimal package.json so the JS/TS workflow takes over
|
|
const workspaceName = basename(process.cwd());
|
|
writeJsonFile('package.json', {
|
|
name: workspaceName,
|
|
version: '0.0.0',
|
|
private: true,
|
|
});
|
|
} else {
|
|
options.useDotNxInstallation = true;
|
|
}
|
|
}
|
|
|
|
const _isNonJs = !existsSync('package.json') || options.useDotNxInstallation;
|
|
const packageJson: PackageJson = _isNonJs
|
|
? null
|
|
: readJsonFile('package.json');
|
|
const _isTurborepo = existsSync('turbo.json');
|
|
const _isMonorepo = _isNonJs ? false : isMonorepo(packageJson);
|
|
|
|
// AI mode defaults to minimum setup, humans can choose
|
|
let guided = !aiMode; // Default to minimum (false) for AI, guided (true) for humans
|
|
if (options.interactive && !(_isTurborepo || _isNonJs)) {
|
|
const setupType = await prompt<{ setupPreference: string }>([
|
|
{
|
|
type: 'select',
|
|
name: 'setupPreference',
|
|
message: 'Would you like a minimum or guided setup?',
|
|
choices: [{ name: 'Minimum' }, { name: 'Guided' }],
|
|
},
|
|
]).then((r) => r.setupPreference);
|
|
guided = setupType === 'Guided';
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
if (aiMode) {
|
|
logProgress('detecting', 'Detected Turborepo project');
|
|
}
|
|
await addNxToTurborepo({
|
|
interactive: options.interactive,
|
|
});
|
|
printFinalMessage({
|
|
learnMoreLink: 'https://nx.dev/recipes/adopting-nx/from-turborepo',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const pmc = getPackageManagerCommand();
|
|
|
|
if (_isMonorepo) {
|
|
if (aiMode) {
|
|
logProgress('detecting', 'Detected monorepo project');
|
|
}
|
|
await addNxToMonorepo(
|
|
{
|
|
interactive: options.interactive,
|
|
nxCloud: false,
|
|
cacheable: options.cacheable,
|
|
},
|
|
guided
|
|
);
|
|
} else if (_isNonJs) {
|
|
if (aiMode) {
|
|
logProgress('detecting', 'Detected non-JavaScript project');
|
|
}
|
|
generateDotNxSetup(version);
|
|
console.log('');
|
|
} else {
|
|
if (aiMode) {
|
|
logProgress('detecting', 'Detected NPM project');
|
|
}
|
|
await addNxToNpmRepo(
|
|
{
|
|
interactive: options.interactive,
|
|
nxCloud: false,
|
|
cacheable: options.cacheable,
|
|
},
|
|
guided
|
|
);
|
|
}
|
|
|
|
const repoRoot = process.cwd();
|
|
|
|
if (aiMode) {
|
|
logProgress('configuring', 'Creating nx.json...');
|
|
}
|
|
createNxJsonFile(repoRoot, [], options.cacheable ?? [], {});
|
|
updateGitIgnore(repoRoot);
|
|
|
|
const nxJson = readNxJson(repoRoot);
|
|
|
|
// Handle plugins based on mode and flags
|
|
let pluginsToInstall: string[] = [];
|
|
let updatePackageScripts = false;
|
|
|
|
if (aiMode) {
|
|
// AI mode: handle --plugins flag
|
|
const parsedPlugins = parsePluginsFlag(options.plugins);
|
|
|
|
if (parsedPlugins === 'skip') {
|
|
// Skip plugins entirely
|
|
logProgress('detecting', 'Skipping plugin installation');
|
|
pluginsToInstall = [];
|
|
} else {
|
|
// Need to detect plugins for 'all' or to return needs_input
|
|
logProgress('detecting', 'Checking for recommended plugins...');
|
|
|
|
const { plugins: detectedPluginNames } = await detectPlugins(
|
|
nxJson,
|
|
packageJson,
|
|
false // non-interactive
|
|
);
|
|
|
|
if (parsedPlugins === 'all') {
|
|
// Install all detected plugins
|
|
pluginsToInstall = detectedPluginNames;
|
|
updatePackageScripts = true;
|
|
} else if (Array.isArray(parsedPlugins)) {
|
|
// Install specific plugins from the comma-separated list
|
|
pluginsToInstall = parsedPlugins;
|
|
updatePackageScripts = true;
|
|
} else if (detectedPluginNames.length > 0) {
|
|
// No --plugins flag provided and plugins were detected
|
|
// Return needs_input for AI to ask user
|
|
const detectedPlugins: DetectedPlugin[] = detectedPluginNames.map(
|
|
(name) => ({
|
|
name,
|
|
reason: getPluginReason(name),
|
|
})
|
|
);
|
|
|
|
logProgress(
|
|
'detecting',
|
|
`Detected ${detectedPluginNames.length} plugin(s): ${detectedPluginNames.join(', ')}`
|
|
);
|
|
writeAiOutput(buildNeedsInputResult(detectedPlugins));
|
|
process.exit(0);
|
|
}
|
|
// else: no plugins flag and no plugins detected, proceed with empty array
|
|
}
|
|
|
|
if (pluginsToInstall.length > 0) {
|
|
logProgress('installing', 'Installing Nx packages...');
|
|
|
|
for (const plugin of pluginsToInstall) {
|
|
logProgress('plugins', `Installing ${plugin}...`);
|
|
}
|
|
|
|
installPluginPackages(repoRoot, pmc, pluginsToInstall);
|
|
await configurePlugins(
|
|
pluginsToInstall,
|
|
updatePackageScripts,
|
|
pmc,
|
|
repoRoot,
|
|
options.verbose
|
|
);
|
|
}
|
|
} else if (guided) {
|
|
// Non-AI guided mode: existing behavior with interactive prompts
|
|
output.log({ title: '🧐 Checking dependencies' });
|
|
|
|
const { plugins: _plugins, updatePackageScripts: _updatePackageScripts } =
|
|
await detectPlugins(nxJson, packageJson, options.interactive);
|
|
pluginsToInstall = _plugins;
|
|
updatePackageScripts = _updatePackageScripts;
|
|
|
|
if (pluginsToInstall.length > 0) {
|
|
output.log({ title: '📦 Installing Nx' });
|
|
|
|
installPluginPackages(repoRoot, pmc, pluginsToInstall);
|
|
await configurePlugins(
|
|
pluginsToInstall,
|
|
updatePackageScripts,
|
|
pmc,
|
|
repoRoot,
|
|
options.verbose
|
|
);
|
|
}
|
|
}
|
|
|
|
const selectedAgents = await determineAiAgents(
|
|
options.aiAgents,
|
|
options.interactive && guided
|
|
);
|
|
|
|
if (selectedAgents && selectedAgents.length > 0) {
|
|
const tree = new FsTree(repoRoot, false);
|
|
const aiAgentsCallback = await setupAiAgentsGenerator(tree, {
|
|
directory: '.',
|
|
writeNxCloudRules: options.nxCloud !== false,
|
|
packageVersion: 'latest',
|
|
agents: [...selectedAgents],
|
|
});
|
|
|
|
const changes = tree.listChanges();
|
|
flushChanges(repoRoot, changes);
|
|
|
|
if (aiAgentsCallback) {
|
|
const results = await aiAgentsCallback();
|
|
results.messages.forEach((m) => output.log(m));
|
|
results.errors.forEach((e) => output.error(e));
|
|
}
|
|
}
|
|
|
|
let nxCloudChoice: MessageOptionKey;
|
|
if (options.nxCloud === true) {
|
|
nxCloudChoice = 'yes';
|
|
} else if (options.nxCloud === false) {
|
|
nxCloudChoice = 'skip';
|
|
} else {
|
|
nxCloudChoice = options.interactive
|
|
? await connectExistingRepoToNxCloudPrompt('init', 'setupNxCloud', false)
|
|
: 'skip';
|
|
}
|
|
if (nxCloudChoice === 'yes') {
|
|
await initCloud('nx-init');
|
|
} else if (nxCloudChoice === 'never') {
|
|
setNeverConnectToCloud(repoRoot);
|
|
}
|
|
|
|
const analyticsPrompt = await ensureAnalyticsPreferenceSet(
|
|
repoRoot,
|
|
options.interactive
|
|
);
|
|
|
|
await recordStat({
|
|
command: 'init',
|
|
nxVersion: version,
|
|
useCloud: nxCloudChoice === 'yes',
|
|
meta: {
|
|
type: 'complete',
|
|
nxCloudArg: nxCloudChoice,
|
|
analyticsPrompt,
|
|
nodeVersion: process.versions.node,
|
|
os: process.platform,
|
|
packageManager: detectPackageManager(),
|
|
aiAgent: aiMode,
|
|
isCI: isCI(),
|
|
pluginsInstalled: pluginsToInstall.join(','),
|
|
},
|
|
});
|
|
|
|
// Output success result for AI agents
|
|
if (aiMode) {
|
|
writeAiOutput(
|
|
buildSuccessResult({
|
|
nxVersion: version,
|
|
pluginsInstalled: pluginsToInstall,
|
|
})
|
|
);
|
|
}
|
|
|
|
// Skip human-readable output for AI agents
|
|
if (!aiMode) {
|
|
printFinalMessage({
|
|
learnMoreLink: 'https://nx.dev/getting-started/adding-to-existing',
|
|
appendLines: _isMonorepo
|
|
? [
|
|
`- Read a detailed guide about adding Nx to NPM/YARN/PNPM workspaces: https://nx.dev/recipes/adopting-nx/adding-to-monorepos`,
|
|
`- Learn how Nx helps manage your TypeScript monorepo: https://nx.dev/features/maintain-ts-monorepos`,
|
|
]
|
|
: [],
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a reason for why a plugin was detected.
|
|
* Used for AI `needs_input` output.
|
|
*/
|
|
export function getPluginReason(plugin: string): string {
|
|
const reasonMap: Record<string, string> = {
|
|
'@nx/eslint': 'eslint detected in dependencies',
|
|
'@nx/storybook': 'storybook detected in dependencies',
|
|
'@nx/vite': 'vite detected in dependencies',
|
|
'@nx/vitest': 'vitest detected in dependencies',
|
|
'@nx/webpack': 'webpack detected in dependencies',
|
|
'@nx/rspack': '@rspack/core detected in dependencies',
|
|
'@nx/rollup': 'rollup detected in dependencies',
|
|
'@nx/jest': 'jest detected in dependencies',
|
|
'@nx/cypress': 'cypress detected in dependencies',
|
|
'@nx/playwright': '@playwright/test detected in dependencies',
|
|
'@nx/detox': 'detox detected in dependencies',
|
|
'@nx/expo': 'expo detected in dependencies',
|
|
'@nx/next': 'next.js detected in dependencies',
|
|
'@nx/nuxt': 'nuxt detected in dependencies',
|
|
'@nx/react-native': 'react-native detected in dependencies',
|
|
'@nx/remix': '@remix-run/dev detected in dependencies',
|
|
'@nx/rsbuild': '@rsbuild/core detected in dependencies',
|
|
'@nx/react': '@react-router/dev detected in dependencies',
|
|
'@nx/gradle': 'gradlew detected in workspace',
|
|
'@nx/dotnet': '.NET project files detected',
|
|
'@nx/maven': 'maven project files detected',
|
|
'@nx/docker': 'Dockerfile detected in workspace',
|
|
};
|
|
return reasonMap[plugin] || `${plugin} detected`;
|
|
}
|
|
|
|
/**
|
|
* Parse the --plugins flag value.
|
|
* Returns: 'skip' | 'all' | string[] (specific plugins)
|
|
*/
|
|
function parsePluginsFlag(
|
|
value: string | undefined
|
|
): 'skip' | 'all' | string[] | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
if (value === 'skip') {
|
|
return 'skip';
|
|
}
|
|
if (value === 'all') {
|
|
return 'all';
|
|
}
|
|
// Comma-separated list - filter out empty strings from edge cases like "--plugins=" or "--plugins=,"
|
|
return value
|
|
.split(',')
|
|
.map((p) => p.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
const npmPackageToPluginMap: Record<string, `@nx/${string}`> = {
|
|
// Generic JS tools
|
|
eslint: '@nx/eslint',
|
|
storybook: '@nx/storybook',
|
|
// Bundlers
|
|
vite: '@nx/vite',
|
|
vitest: '@nx/vitest',
|
|
webpack: '@nx/webpack',
|
|
'@rspack/core': '@nx/rspack',
|
|
rollup: '@nx/rollup',
|
|
// Testing tools
|
|
jest: '@nx/jest',
|
|
cypress: '@nx/cypress',
|
|
'@playwright/test': '@nx/playwright',
|
|
// Frameworks
|
|
detox: '@nx/detox',
|
|
expo: '@nx/expo',
|
|
next: '@nx/next',
|
|
nuxt: '@nx/nuxt',
|
|
'react-native': '@nx/react-native',
|
|
'@remix-run/dev': '@nx/remix',
|
|
'@rsbuild/core': '@nx/rsbuild',
|
|
'@react-router/dev': '@nx/react',
|
|
};
|
|
|
|
export async function detectPlugins(
|
|
nxJson: NxJsonConfiguration,
|
|
packageJson: PackageJson | null,
|
|
interactive: boolean,
|
|
includeAngularCli?: boolean
|
|
): Promise<{
|
|
plugins: string[];
|
|
updatePackageScripts: boolean;
|
|
}> {
|
|
let files = ['package.json'].concat(
|
|
globWithWorkspaceContextSync(process.cwd(), ['**/*/package.json'])
|
|
);
|
|
|
|
const currentPlugins = new Set(
|
|
(nxJson.plugins ?? []).map((p) => {
|
|
const plugin = typeof p === 'string' ? p : p.plugin;
|
|
return getPackageNameFromImportPath(plugin);
|
|
})
|
|
);
|
|
|
|
// Also treat already-installed @nx/* and @nrwl/* packages as current plugins
|
|
const rootDeps = {
|
|
...packageJson?.dependencies,
|
|
...packageJson?.devDependencies,
|
|
};
|
|
for (const dep of Object.keys(rootDeps)) {
|
|
if (dep.startsWith('@nx/') || dep.startsWith('@nrwl/')) {
|
|
currentPlugins.add(getPackageNameFromImportPath(dep));
|
|
}
|
|
}
|
|
|
|
const detectedPlugins = new Set<string>();
|
|
for (const file of files) {
|
|
if (!existsSync(file)) continue;
|
|
|
|
let packageJson: PackageJson;
|
|
try {
|
|
packageJson = readJsonFile(file);
|
|
} catch {
|
|
// Could have malformed JSON for unit tests, etc.
|
|
continue;
|
|
}
|
|
|
|
const deps = {
|
|
...packageJson.dependencies,
|
|
...packageJson.devDependencies,
|
|
};
|
|
|
|
const _npmPackageToPluginMap = {
|
|
...npmPackageToPluginMap,
|
|
};
|
|
if (includeAngularCli) {
|
|
_npmPackageToPluginMap['@angular/cli'] = '@nx/angular';
|
|
}
|
|
for (const [dep, plugin] of Object.entries(_npmPackageToPluginMap)) {
|
|
if (deps[dep]) {
|
|
detectedPlugins.add(plugin);
|
|
}
|
|
}
|
|
}
|
|
|
|
let gradlewFiles = ['gradlew', 'gradlew.bat'].concat(
|
|
globWithWorkspaceContextSync(process.cwd(), [
|
|
'**/gradlew',
|
|
'**/gradlew.bat',
|
|
])
|
|
);
|
|
if (gradlewFiles.some((f) => existsSync(f))) {
|
|
detectedPlugins.add('@nx/gradle');
|
|
}
|
|
|
|
const dotnetProjectGlobs = ['**/*.csproj', '**/*.fsproj', '**/*.vbproj'];
|
|
const dotnetFiles = globWithWorkspaceContextSync(process.cwd(), [
|
|
...dotnetProjectGlobs,
|
|
]);
|
|
if (dotnetFiles.length > 0) {
|
|
detectedPlugins.add('@nx/dotnet');
|
|
}
|
|
|
|
let mvnwFiles = globWithWorkspaceContextSync(process.cwd(), [
|
|
'mvnw',
|
|
'mvnw.cmd',
|
|
'pom.xml',
|
|
'**/mvnw',
|
|
'**/mvnw.cmd',
|
|
'**/pom.xml',
|
|
]);
|
|
if (mvnwFiles.length > 0) {
|
|
detectedPlugins.add('@nx/maven');
|
|
}
|
|
|
|
let dockerFiles = ['Dockerfile'].concat(
|
|
globWithWorkspaceContextSync(process.cwd(), ['**/Dockerfile'])
|
|
);
|
|
if (dockerFiles.some((f) => existsSync(f))) {
|
|
detectedPlugins.add('@nx/docker');
|
|
}
|
|
|
|
// Remove existing plugins
|
|
for (const plugin of detectedPlugins) {
|
|
if (currentPlugins.has(plugin)) {
|
|
detectedPlugins.delete(plugin);
|
|
}
|
|
}
|
|
|
|
const plugins = Array.from(detectedPlugins);
|
|
|
|
if (plugins.length === 0) {
|
|
return {
|
|
plugins: [],
|
|
updatePackageScripts: false,
|
|
};
|
|
}
|
|
|
|
if (!interactive) {
|
|
output.log({
|
|
title: `Recommended Plugins:`,
|
|
bodyLines: [
|
|
`Adding these Nx plugins to integrate with the tools used in your workspace:`,
|
|
...plugins.map((p) => `- ${p}`),
|
|
],
|
|
});
|
|
return {
|
|
plugins,
|
|
updatePackageScripts: true,
|
|
};
|
|
}
|
|
|
|
output.log({
|
|
title: `Recommended Plugins:`,
|
|
bodyLines: [
|
|
`Add these Nx plugins to integrate with the tools used in your workspace.`,
|
|
],
|
|
});
|
|
|
|
const pluginsToInstall = await prompt<{ plugins: string[] }>([
|
|
{
|
|
name: 'plugins',
|
|
type: 'multiselect',
|
|
message: `Which plugins would you like to add? Press <Space> to select and <Enter> to submit.`,
|
|
choices: plugins.map((p) => ({ name: p, value: p })),
|
|
/**
|
|
* limit is missing from the interface but it limits the amount of options shown
|
|
*/
|
|
limit: process.stdout.rows - 4, // 4 leaves room for the header above, the prompt and some whitespace
|
|
} as any,
|
|
]).then((r) => r.plugins);
|
|
|
|
if (pluginsToInstall?.length === 0)
|
|
return {
|
|
plugins: [],
|
|
updatePackageScripts: false,
|
|
};
|
|
|
|
const updatePackageScripts =
|
|
existsSync('package.json') &&
|
|
(await prompt<{ updatePackageScripts: string }>([
|
|
{
|
|
name: 'updatePackageScripts',
|
|
type: 'autocomplete',
|
|
message: `Do you want to start using Nx in your package.json scripts?`,
|
|
choices: [
|
|
{
|
|
name: 'Yes',
|
|
},
|
|
{
|
|
name: 'No',
|
|
},
|
|
],
|
|
initial: 0,
|
|
},
|
|
]).then((r) => r.updatePackageScripts === 'Yes'));
|
|
|
|
return { plugins: pluginsToInstall, updatePackageScripts };
|
|
}
|