-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6e8a3e5
commit fb3d93f
Showing
15 changed files
with
925 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { | ||
checkFilesExist, | ||
cleanupProject, | ||
getSelectedPackageManager, | ||
newProject, | ||
runCLI, | ||
updateJson, | ||
updateFile, | ||
} from '@nx/e2e/utils'; | ||
|
||
describe('Nx Import', () => { | ||
let proj: string; | ||
beforeAll( | ||
() => | ||
(proj = newProject({ | ||
packages: ['@nx/js'], | ||
unsetProjectNameAndRootFormat: false, | ||
})) | ||
); | ||
afterAll(() => cleanupProject()); | ||
|
||
it('should be able to import a vite app', () => { | ||
const remote = 'https://github.com/nrwl/nx.git'; | ||
const ref = 'e2e-test/import-vite'; | ||
const source = '.'; | ||
const directory = 'projects/vite-app'; | ||
|
||
if (getSelectedPackageManager() === 'pnpm') { | ||
updateFile( | ||
'pnpm-workspaces.yaml', | ||
`packages: | ||
- 'projects/*' | ||
` | ||
); | ||
} else { | ||
updateJson('package.json', (json) => { | ||
json.workspaces = ['projects/*']; | ||
return json; | ||
}); | ||
} | ||
|
||
runCLI( | ||
`import ${remote} ${directory} --ref ${ref} --source ${source} --no-interactive`, | ||
{ | ||
verbose: true, | ||
} | ||
); | ||
|
||
checkFilesExist( | ||
'projects/vite-app/.gitignore', | ||
'projects/vite-app/package.json', | ||
'projects/vite-app/index.html', | ||
'projects/vite-app/vite.config.ts', | ||
'projects/vite-app/src/main.tsx', | ||
'projects/vite-app/src/App.tsx' | ||
); | ||
runCLI(`vite:build created-vite-app`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { CommandModule } from 'yargs'; | ||
import { linkToNxDevAndExamples } from '../yargs-utils/documentation'; | ||
import { withVerbose } from '../yargs-utils/shared-options'; | ||
import { handleErrors } from '../../utils/params'; | ||
|
||
export const yargsImportCommand: CommandModule = { | ||
command: 'import [sourceRemoteUrl] [destination]', | ||
describe: false, | ||
builder: (yargs) => | ||
linkToNxDevAndExamples( | ||
withVerbose( | ||
yargs | ||
.positional('sourceRemoteUrl', { | ||
type: 'string', | ||
description: 'The remote URL of the source to import', | ||
}) | ||
.positional('destination', { | ||
type: 'string', | ||
description: | ||
'The directory in the current workspace to import into', | ||
}) | ||
.option('source', { | ||
type: 'string', | ||
description: | ||
'The directory in the source repository to import from', | ||
}) | ||
.option('ref', { | ||
type: 'string', | ||
description: 'The branch from the source repository to import', | ||
}) | ||
.option('interactive', { | ||
type: 'boolean', | ||
description: 'Interactive mode', | ||
default: true, | ||
}) | ||
), | ||
'import' | ||
), | ||
handler: async (args) => { | ||
const exitCode = await handleErrors( | ||
(args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true', | ||
async () => { | ||
return (await import('./import')).importHandler(args as any); | ||
} | ||
); | ||
process.exit(exitCode); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
import { join, relative } from 'path'; | ||
import { cloneFromUpstream, GitRepository } from '../../utils/git-utils'; | ||
import { mkdir, readdir, rm } from 'fs'; | ||
import { promisify } from 'util'; | ||
import { tmpdir } from 'tmp'; | ||
import { prompt } from 'enquirer'; | ||
import { output } from '../../utils/output'; | ||
import * as createSpinner from 'ora'; | ||
import { detectPlugins, installPlugins } from '../init/init-v2'; | ||
import { readNxJson } from '../../config/nx-json'; | ||
import { workspaceRoot } from '../../utils/workspace-root'; | ||
import { | ||
detectPackageManager, | ||
getPackageManagerCommand, | ||
} from '../../utils/package-manager'; | ||
import { resetWorkspaceContext } from '../../utils/workspace-context'; | ||
import { runInstall } from '../init/implementation/utils'; | ||
import { getBaseRef } from '../../utils/command-line-utils'; | ||
import { prepareSourceRepo } from './utils/prepare-source-repo'; | ||
import { mergeRemoteSource } from './utils/merge-remote-source'; | ||
import { | ||
getPackagesInPackageManagerWorkspace, | ||
needsInstall, | ||
} from './utils/needs-install'; | ||
|
||
const rmAsync = promisify(rm); | ||
const mkdirAsync = promisify(mkdir); | ||
const readdirAsync = promisify(readdir); | ||
|
||
const importRemoteName = '__tmp_nx_import__'; | ||
|
||
export interface ImportOptions { | ||
/** | ||
* The remote URL of the repository to import | ||
*/ | ||
sourceRemoteUrl: string; | ||
/** | ||
* The branch or reference to import | ||
*/ | ||
ref: string; | ||
/** | ||
* The directory in the source repo to import | ||
*/ | ||
source: string; | ||
/** | ||
* The directory in the destination repo to import into | ||
*/ | ||
destination: string; | ||
|
||
verbose: boolean; | ||
interactive: boolean; | ||
} | ||
|
||
export async function importHandler(options: ImportOptions) { | ||
let { sourceRemoteUrl, ref, source, destination } = options; | ||
|
||
output.log({ | ||
title: | ||
'Nx will walk you through the process of importing code from another repository into this workspace:', | ||
bodyLines: [ | ||
`1. Nx will clone the other repository into a temporary directory`, | ||
`2. Code to be imported will be moved to the same directory it will be imported into on a temporary branch`, | ||
`3. The code will be merged into the current branch in this workspace`, | ||
`4. Nx will recommend plugins to integrate tools used in the imported code with Nx`, | ||
`5. The code will be successfully imported into this workspace`, | ||
'', | ||
`Git history will be preserved during this process`, | ||
], | ||
}); | ||
|
||
const tempImportDirectory = join(tmpdir, 'nx-import'); | ||
|
||
if (!sourceRemoteUrl) { | ||
sourceRemoteUrl = ( | ||
await prompt<{ sourceRemoteUrl: string }>([ | ||
{ | ||
type: 'input', | ||
name: 'sourceRemoteUrl', | ||
message: | ||
'What is the Remote URL of the repository you want to import?', | ||
required: true, | ||
}, | ||
]) | ||
).sourceRemoteUrl; | ||
} | ||
|
||
const sourceRepoPath = join(tempImportDirectory, 'repo'); | ||
const spinner = createSpinner( | ||
`Cloning ${sourceRemoteUrl} into a temporary directory: ${sourceRepoPath}` | ||
).start(); | ||
try { | ||
await rmAsync(tempImportDirectory, { recursive: true }); | ||
} catch {} | ||
await mkdirAsync(tempImportDirectory, { recursive: true }); | ||
|
||
const sourceGitClient = await cloneFromUpstream( | ||
sourceRemoteUrl, | ||
sourceRepoPath | ||
); | ||
spinner.succeed(`Cloned into ${tempImportDirectory}`); | ||
|
||
if (!ref) { | ||
const branchChoices = await sourceGitClient.listBranches(); | ||
ref = ( | ||
await prompt<{ ref: string }>([ | ||
{ | ||
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, | ||
} as any, | ||
]) | ||
).ref; | ||
} | ||
|
||
if (!source) { | ||
source = ( | ||
await prompt<{ source: string }>([ | ||
{ | ||
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 prompt<{ destination: string }>([ | ||
{ | ||
type: 'input', | ||
name: 'destination', | ||
message: 'Where in this workspace should the code be imported into?', | ||
required: true, | ||
}, | ||
]) | ||
).destination; | ||
} | ||
|
||
const absDestination = join(process.cwd(), destination); | ||
|
||
await assertDestinationEmpty(absDestination); | ||
|
||
const tempImportBranch = getTempImportBranch(ref); | ||
|
||
const packageManager = detectPackageManager(workspaceRoot); | ||
|
||
const originalPackageWorkspaces = await getPackagesInPackageManagerWorkspace( | ||
packageManager | ||
); | ||
const destinationGitClient = new GitRepository(process.cwd()); | ||
|
||
const relativeDestination = relative( | ||
destinationGitClient.root, | ||
absDestination | ||
); | ||
if (process.env.NX_IMPORT_SKIP_SOURCE_PREPARATION !== 'true') { | ||
await prepareSourceRepo( | ||
sourceGitClient, | ||
ref, | ||
source, | ||
relativeDestination, | ||
tempImportBranch, | ||
sourceRemoteUrl | ||
); | ||
|
||
await createTemporaryRemote( | ||
destinationGitClient, | ||
join(sourceRepoPath, '.git'), | ||
importRemoteName | ||
); | ||
} | ||
|
||
await mergeRemoteSource( | ||
destinationGitClient, | ||
sourceRemoteUrl, | ||
tempImportBranch, | ||
destination, | ||
importRemoteName, | ||
ref | ||
); | ||
|
||
spinner.start('Cleaning up temporary files and remotes'); | ||
await rmAsync(tempImportDirectory, { recursive: true }); | ||
await destinationGitClient.deleteGitRemote(importRemoteName); | ||
spinner.succeed('Cleaned up temporary files and remotes'); | ||
|
||
const pmc = getPackageManagerCommand(); | ||
const nxJson = readNxJson(workspaceRoot); | ||
|
||
resetWorkspaceContext(); | ||
|
||
const { plugins, updatePackageScripts } = await detectPlugins( | ||
nxJson, | ||
options.interactive | ||
); | ||
|
||
if (plugins.length > 0) { | ||
output.log({ title: 'Installing Plugins' }); | ||
installPlugins(workspaceRoot, plugins, pmc, updatePackageScripts); | ||
|
||
await destinationGitClient.amendCommit(); | ||
} else if (await needsInstall(packageManager, originalPackageWorkspaces)) { | ||
output.log({ | ||
title: 'Installing dependencies for imported code', | ||
}); | ||
|
||
runInstall(workspaceRoot, getPackageManagerCommand(packageManager)); | ||
|
||
await destinationGitClient.amendCommit(); | ||
} | ||
|
||
console.log(await destinationGitClient.showStat()); | ||
|
||
output.log({ | ||
title: `Merging these changes into ${getBaseRef(nxJson)}`, | ||
bodyLines: [ | ||
`MERGE these changes when merging these changes.`, | ||
`Do NOT squash and do NOT rebase these changes when merging these changes.`, | ||
`If you would like to UNDO these changes, run "git reset HEAD~1 --hard"`, | ||
], | ||
}); | ||
} | ||
|
||
async function assertDestinationEmpty(absDestination: string) { | ||
try { | ||
const files = await readdirAsync(absDestination); | ||
if (files.length > 0) { | ||
throw new Error( | ||
`Destination directory ${absDestination} is not empty. Please make sure it is empty before importing into it.` | ||
); | ||
} | ||
} catch { | ||
// Directory does not exist and that's OK | ||
} | ||
} | ||
|
||
function getTempImportBranch(sourceBranch: string) { | ||
return `__nx_tmp_import__/${sourceBranch}`; | ||
} | ||
|
||
async function createTemporaryRemote( | ||
destinationGitClient: GitRepository, | ||
sourceRemoteUrl: string, | ||
remoteName: string | ||
) { | ||
try { | ||
await destinationGitClient.deleteGitRemote(remoteName); | ||
} catch {} | ||
await destinationGitClient.addGitRemote(remoteName, sourceRemoteUrl); | ||
await destinationGitClient.fetch(remoteName, { | ||
depth: 1, | ||
}); | ||
} |
Oops, something went wrong.