nx
Version:
329 lines (328 loc) • 16.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.importHandler = importHandler;
const path_1 = require("path");
const minimatch_1 = require("minimatch");
const node_fs_1 = require("node:fs");
const chalk = require("chalk");
const js_yaml_1 = require("@zkochan/js-yaml");
const git_utils_1 = require("../../utils/git-utils");
const promises_1 = require("node:fs/promises");
const tmp_1 = require("tmp");
const enquirer_1 = require("enquirer");
const output_1 = require("../../utils/output");
const createSpinner = require("ora");
const init_v2_1 = require("../init/init-v2");
const nx_json_1 = require("../../config/nx-json");
const workspace_root_1 = require("../../utils/workspace-root");
const package_manager_1 = require("../../utils/package-manager");
const workspace_context_1 = require("../../utils/workspace-context");
const utils_1 = require("../init/implementation/utils");
const command_line_utils_1 = require("../../utils/command-line-utils");
const prepare_source_repo_1 = require("./utils/prepare-source-repo");
const merge_remote_source_1 = require("./utils/merge-remote-source");
const needs_install_1 = require("./utils/needs-install");
const file_utils_1 = require("../../project-graph/file-utils");
const importRemoteName = '__tmp_nx_import__';
async function importHandler(options) {
process.env.NX_RUNNING_NX_IMPORT = 'true';
let { sourceRepository, ref, source, destination } = options;
const destinationGitClient = new git_utils_1.GitRepository(process.cwd());
if (await destinationGitClient.hasUncommittedChanges()) {
throw new Error(`You have uncommitted changes in the destination repository. Commit or revert the changes and try again.`);
}
output_1.output.log({
title: 'Nx will walk you through the process of importing code from the source repository into this repository:',
bodyLines: [
`1. Nx will clone the source repository into a temporary directory`,
`2. The project code from the sourceDirectory will be moved to the destinationDirectory on a temporary branch in this repository`,
`3. The temporary branch will be merged into the current branch in this repository`,
`4. Nx will recommend plugins to integrate any new tools used in the imported code`,
'',
`Git history will be preserved during this process as long as you MERGE these changes. Do NOT squash and do NOT rebase the changes when merging branches. If you would like to UNDO these changes, run "git reset HEAD~1 --hard"`,
],
});
const tempImportDirectory = (0, path_1.join)(tmp_1.tmpdir, 'nx-import');
if (!sourceRepository) {
sourceRepository = (await (0, enquirer_1.prompt)([
{
type: 'input',
name: 'sourceRepository',
message: 'What is the URL of the repository you want to import? (This can be a local git repository or a git remote URL)',
required: true,
},
])).sourceRepository;
}
try {
const maybeLocalDirectory = await (0, promises_1.stat)(sourceRepository);
if (maybeLocalDirectory.isDirectory()) {
sourceRepository = (0, path_1.resolve)(sourceRepository);
}
}
catch (e) {
// It's a remote url
}
const sourceTempRepoPath = (0, path_1.join)(tempImportDirectory, 'repo');
const spinner = createSpinner(`Cloning ${sourceRepository} into a temporary directory: ${sourceTempRepoPath} (Use --depth to limit commit history and speed up clone times)`).start();
try {
await (0, promises_1.rm)(tempImportDirectory, { recursive: true });
}
catch { }
await (0, promises_1.mkdir)(tempImportDirectory, { recursive: true });
let sourceGitClient;
try {
sourceGitClient = await (0, git_utils_1.cloneFromUpstream)(sourceRepository, sourceTempRepoPath, {
originName: importRemoteName,
depth: options.depth,
});
}
catch (e) {
spinner.fail(`Failed to clone ${sourceRepository} into ${sourceTempRepoPath}`);
let errorMessage = `Failed to clone ${sourceRepository} into ${sourceTempRepoPath}. Please double check the remote and try again.\n${e.message}`;
throw new Error(errorMessage);
}
spinner.succeed(`Cloned into ${sourceTempRepoPath}`);
// Detecting the package manager before preparing the source repo for import.
const sourcePackageManager = (0, package_manager_1.detectPackageManager)(sourceGitClient.root);
if (!ref) {
const branchChoices = await sourceGitClient.listBranches();
ref = (await (0, enquirer_1.prompt)([
{
type: 'autocomplete',
name: 'ref',
message: `Which branch do you want to import?`,
choices: branchChoices,
/**
* Limit the number of choices so that it fits on screen
*/
limit: process.stdout.rows - 3,
required: true,
},
])).ref;
}
if (!source) {
source = (await (0, enquirer_1.prompt)([
{
type: 'input',
name: 'source',
message: `Which directory do you want to import into this workspace? (leave blank to import the entire repository)`,
},
])).source;
}
if (!destination) {
destination = (await (0, enquirer_1.prompt)([
{
type: 'input',
name: 'destination',
message: 'Where in this workspace should the code be imported into?',
required: true,
initial: source ? source : undefined,
},
])).destination;
}
const absSource = (0, path_1.join)(sourceTempRepoPath, source);
if ((0, path_1.isAbsolute)(destination)) {
throw new Error(`The destination directory must be a relative path in this repository.`);
}
const absDestination = (0, path_1.join)(process.cwd(), destination);
await assertDestinationEmpty(destinationGitClient, absDestination);
const tempImportBranch = getTempImportBranch(ref);
await sourceGitClient.addFetchRemote(importRemoteName, ref);
await sourceGitClient.fetch(importRemoteName, ref);
spinner.succeed(`Fetched ${ref} from ${sourceRepository}`);
spinner.start(`Checking out a temporary branch, ${tempImportBranch} based on ${ref}`);
await sourceGitClient.checkout(tempImportBranch, {
new: true,
base: `${importRemoteName}/${ref}`,
});
spinner.succeed(`Created a ${tempImportBranch} branch based on ${ref}`);
try {
await (0, promises_1.stat)(absSource);
}
catch (e) {
throw new Error(`The source directory ${source} does not exist in ${sourceRepository}. Please double check to make sure it exists.`);
}
const packageManager = (0, package_manager_1.detectPackageManager)(workspace_root_1.workspaceRoot);
const originalPackageWorkspaces = await (0, needs_install_1.getPackagesInPackageManagerWorkspace)(packageManager);
const sourceIsNxWorkspace = (0, node_fs_1.existsSync)((0, path_1.join)(sourceGitClient.root, 'nx.json'));
const relativeDestination = (0, path_1.relative)(destinationGitClient.root, absDestination);
await (0, prepare_source_repo_1.prepareSourceRepo)(sourceGitClient, ref, source, relativeDestination, tempImportBranch, sourceRepository);
await createTemporaryRemote(destinationGitClient, (0, path_1.join)(sourceTempRepoPath, '.git'), importRemoteName);
await (0, merge_remote_source_1.mergeRemoteSource)(destinationGitClient, sourceRepository, tempImportBranch, destination, importRemoteName, ref);
spinner.start('Cleaning up temporary files and remotes');
await (0, promises_1.rm)(tempImportDirectory, { recursive: true });
await destinationGitClient.deleteGitRemote(importRemoteName);
spinner.succeed('Cleaned up temporary files and remotes');
const pmc = (0, package_manager_1.getPackageManagerCommand)();
const nxJson = (0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot);
(0, workspace_context_1.resetWorkspaceContext)();
const { plugins, updatePackageScripts } = await (0, init_v2_1.detectPlugins)(nxJson, options.interactive, true);
if (packageManager !== sourcePackageManager) {
output_1.output.warn({
title: `Mismatched package managers`,
bodyLines: [
`The source repository is using a different package manager (${sourcePackageManager}) than this workspace (${packageManager}).`,
`This could lead to install issues due to discrepancies in "package.json" features.`,
],
});
}
// If install fails, we should continue since the errors could be resolved later.
let installFailed = false;
if (plugins.length > 0) {
try {
output_1.output.log({ title: 'Installing Plugins' });
(0, init_v2_1.installPlugins)(workspace_root_1.workspaceRoot, plugins, pmc, updatePackageScripts);
await destinationGitClient.amendCommit();
}
catch (e) {
installFailed = true;
output_1.output.error({
title: `Install failed: ${e.message || 'Unknown error'}`,
bodyLines: [e.stack],
});
}
}
else if (await (0, needs_install_1.needsInstall)(packageManager, originalPackageWorkspaces)) {
try {
output_1.output.log({
title: 'Installing dependencies for imported code',
});
(0, utils_1.runInstall)(workspace_root_1.workspaceRoot, (0, package_manager_1.getPackageManagerCommand)(packageManager));
await destinationGitClient.amendCommit();
}
catch (e) {
installFailed = true;
output_1.output.error({
title: `Install failed: ${e.message || 'Unknown error'}`,
bodyLines: [e.stack],
});
}
}
console.log(await destinationGitClient.showStat());
if (installFailed) {
const pmc = (0, package_manager_1.getPackageManagerCommand)(packageManager);
output_1.output.warn({
title: `The import was successful, but the install failed`,
bodyLines: [
`You may need to run "${pmc.install}" manually to resolve the issue. The error is logged above.`,
],
});
}
await warnOnMissingWorkspacesEntry(packageManager, pmc, relativeDestination);
if (source != destination) {
output_1.output.warn({
title: `Check configuration files`,
bodyLines: [
`The source directory (${source}) and destination directory (${destination}) are different.`,
`You may need to update configuration files to match the directory in this repository.`,
sourceIsNxWorkspace
? `For example, path options in project.json such as "main", "tsConfig", and "outputPath" need to be updated.`
: `For example, relative paths in tsconfig.json and other tooling configuration files may need to be updated.`,
],
});
}
// When only a subdirectory is imported, there might be devDependencies in the root package.json file
// that needs to be ported over as well.
if (ref) {
output_1.output.log({
title: `Check root dependencies`,
bodyLines: [
`"dependencies" and "devDependencies" are not imported from the source repository (${sourceRepository}).`,
`You may need to add some of those dependencies to this workspace in order to run tasks successfully.`,
],
});
}
output_1.output.log({
title: `Merging these changes into ${(0, command_line_utils_1.getBaseRef)(nxJson)}`,
bodyLines: [
`MERGE these changes when merging these changes.`,
`Do NOT squash these commits when merging these changes.`,
`If you rebase, make sure to use "--rebase-merges" to preserve merge commits.`,
`To UNDO these changes, run "git reset HEAD~1 --hard"`,
],
});
}
async function assertDestinationEmpty(gitClient, absDestination) {
const files = await gitClient.getGitFiles(absDestination);
if (files.length > 0) {
throw new Error(`Destination directory ${absDestination} is not empty. Please make sure it is empty before importing into it.`);
}
}
function getTempImportBranch(sourceBranch) {
return `__nx_tmp_import__/${sourceBranch}`;
}
async function createTemporaryRemote(destinationGitClient, sourceRemoteUrl, remoteName) {
try {
await destinationGitClient.deleteGitRemote(remoteName);
}
catch { }
await destinationGitClient.addGitRemote(remoteName, sourceRemoteUrl);
await destinationGitClient.fetch(remoteName);
}
// If the user imports a project that isn't in NPM/Yarn/PNPM workspaces, then its dependencies
// will not be installed. We should warn users and provide instructions on how to fix this.
async function warnOnMissingWorkspacesEntry(pm, pmc, pkgPath) {
if (!(0, package_manager_1.isWorkspacesEnabled)(pm, workspace_root_1.workspaceRoot)) {
output_1.output.warn({
title: `Missing workspaces in package.json`,
bodyLines: pm === 'npm'
? [
`We recommend enabling NPM workspaces to install dependencies for the imported project.`,
`Add \`"workspaces": ["${pkgPath}"]\` to package.json and run "${pmc.install}".`,
`See: https://docs.npmjs.com/cli/using-npm/workspaces`,
]
: pm === 'yarn'
? [
`We recommend enabling Yarn workspaces to install dependencies for the imported project.`,
`Add \`"workspaces": ["${pkgPath}"]\` to package.json and run "${pmc.install}".`,
`See: https://yarnpkg.com/features/workspaces`,
]
: pm === 'bun'
? [
`We recommend enabling Bun workspaces to install dependencies for the imported project.`,
`Add \`"workspaces": ["${pkgPath}"]\` to package.json and run "${pmc.install}".`,
`See: https://bun.sh/docs/install/workspaces`,
]
: [
`We recommend enabling PNPM workspaces to install dependencies for the imported project.`,
`Add the following entry to to pnpm-workspace.yaml and run "${pmc.install}":`,
chalk.bold(`packages:\n - '${pkgPath}'`),
`See: https://pnpm.io/workspaces`,
],
});
}
else {
// Check if the new package is included in existing workspaces entries. If not, warn the user.
let workspaces = null;
if (pm === 'npm' || pm === 'yarn' || pm === 'bun') {
const packageJson = (0, file_utils_1.readPackageJson)();
workspaces = packageJson.workspaces;
}
else if (pm === 'pnpm') {
const yamlPath = (0, path_1.join)(workspace_root_1.workspaceRoot, 'pnpm-workspace.yaml');
if ((0, node_fs_1.existsSync)(yamlPath)) {
const yamlContent = await node_fs_1.promises.readFile(yamlPath, 'utf-8');
const yaml = (0, js_yaml_1.load)(yamlContent);
workspaces = yaml.packages;
}
}
if (workspaces) {
const isPkgIncluded = workspaces.some((w) => (0, minimatch_1.minimatch)(pkgPath, w));
if (!isPkgIncluded) {
const pkgsDir = (0, path_1.dirname)(pkgPath);
output_1.output.warn({
title: `Project missing in workspaces`,
bodyLines: pm === 'npm' || pm === 'yarn' || pm === 'bun'
? [
`The imported project (${pkgPath}) is missing the "workspaces" field in package.json.`,
`Add "${pkgsDir}/*" to workspaces run "${pmc.install}".`,
]
: [
`The imported project (${pkgPath}) is missing the "packages" field in pnpm-workspaces.yaml.`,
`Add "${pkgsDir}/*" to packages run "${pmc.install}".`,
],
});
}
}
}
}