1
- import { join , relative , resolve } from 'path' ;
1
+ import { dirname , join , relative , resolve } from 'path' ;
2
+ import { minimatch } from 'minimatch' ;
3
+ import { existsSync , promises as fsp } from 'node:fs' ;
4
+ import * as chalk from 'chalk' ;
5
+ import { load as yamlLoad } from '@zkochan/js-yaml' ;
2
6
import { cloneFromUpstream , GitRepository } from '../../utils/git-utils' ;
3
7
import { stat , mkdir , rm } from 'node:fs/promises' ;
4
8
import { tmpdir } from 'tmp' ;
@@ -11,6 +15,9 @@ import { workspaceRoot } from '../../utils/workspace-root';
11
15
import {
12
16
detectPackageManager ,
13
17
getPackageManagerCommand ,
18
+ isWorkspacesEnabled ,
19
+ PackageManager ,
20
+ PackageManagerCommands ,
14
21
} from '../../utils/package-manager' ;
15
22
import { resetWorkspaceContext } from '../../utils/workspace-context' ;
16
23
import { runInstall } from '../init/implementation/utils' ;
@@ -21,6 +28,7 @@ import {
21
28
getPackagesInPackageManagerWorkspace ,
22
29
needsInstall ,
23
30
} from './utils/needs-install' ;
31
+ import { readPackageJson } from '../../project-graph/file-utils' ;
24
32
25
33
const importRemoteName = '__tmp_nx_import__' ;
26
34
@@ -41,6 +49,10 @@ export interface ImportOptions {
41
49
* The directory in the destination repo to import into
42
50
*/
43
51
destination : string ;
52
+ /**
53
+ * The depth to clone the source repository (limit this for faster clone times)
54
+ */
55
+ depth : number ;
44
56
45
57
verbose : boolean ;
46
58
interactive : boolean ;
@@ -90,7 +102,7 @@ export async function importHandler(options: ImportOptions) {
90
102
91
103
const sourceRepoPath = join ( tempImportDirectory , 'repo' ) ;
92
104
const spinner = createSpinner (
93
- `Cloning ${ sourceRemoteUrl } into a temporary directory: ${ sourceRepoPath } `
105
+ `Cloning ${ sourceRemoteUrl } into a temporary directory: ${ sourceRepoPath } (Use --depth to limit commit history and speed up clone times) `
94
106
) . start ( ) ;
95
107
try {
96
108
await rm ( tempImportDirectory , { recursive : true } ) ;
@@ -101,6 +113,7 @@ export async function importHandler(options: ImportOptions) {
101
113
try {
102
114
sourceGitClient = await cloneFromUpstream ( sourceRemoteUrl , sourceRepoPath , {
103
115
originName : importRemoteName ,
116
+ depth : options . depth ,
104
117
} ) ;
105
118
} catch ( e ) {
106
119
spinner . fail ( `Failed to clone ${ sourceRemoteUrl } into ${ sourceRepoPath } ` ) ;
@@ -110,6 +123,9 @@ export async function importHandler(options: ImportOptions) {
110
123
}
111
124
spinner . succeed ( `Cloned into ${ sourceRepoPath } ` ) ;
112
125
126
+ // Detecting the package manager before preparing the source repo for import.
127
+ const sourcePackageManager = detectPackageManager ( sourceGitClient . root ) ;
128
+
113
129
if ( ! ref ) {
114
130
const branchChoices = await sourceGitClient . listBranches ( ) ;
115
131
ref = (
@@ -149,6 +165,7 @@ export async function importHandler(options: ImportOptions) {
149
165
name : 'destination' ,
150
166
message : 'Where in this workspace should the code be imported into?' ,
151
167
required : true ,
168
+ initial : source ? source : undefined ,
152
169
} ,
153
170
] )
154
171
) . destination ;
@@ -157,6 +174,23 @@ export async function importHandler(options: ImportOptions) {
157
174
const absSource = join ( sourceRepoPath , source ) ;
158
175
const absDestination = join ( process . cwd ( ) , destination ) ;
159
176
177
+ const destinationGitClient = new GitRepository ( process . cwd ( ) ) ;
178
+ await assertDestinationEmpty ( destinationGitClient , absDestination ) ;
179
+
180
+ const tempImportBranch = getTempImportBranch ( ref ) ;
181
+ await sourceGitClient . addFetchRemote ( importRemoteName , ref ) ;
182
+ await sourceGitClient . fetch ( importRemoteName , ref ) ;
183
+ spinner . succeed ( `Fetched ${ ref } from ${ sourceRemoteUrl } ` ) ;
184
+ spinner . start (
185
+ `Checking out a temporary branch, ${ tempImportBranch } based on ${ ref } `
186
+ ) ;
187
+ await sourceGitClient . checkout ( tempImportBranch , {
188
+ new : true ,
189
+ base : `${ importRemoteName } /${ ref } ` ,
190
+ } ) ;
191
+
192
+ spinner . succeed ( `Created a ${ tempImportBranch } branch based on ${ ref } ` ) ;
193
+
160
194
try {
161
195
await stat ( absSource ) ;
162
196
} catch ( e ) {
@@ -165,11 +199,6 @@ export async function importHandler(options: ImportOptions) {
165
199
) ;
166
200
}
167
201
168
- const destinationGitClient = new GitRepository ( process . cwd ( ) ) ;
169
- await assertDestinationEmpty ( destinationGitClient , absDestination ) ;
170
-
171
- const tempImportBranch = getTempImportBranch ( ref ) ;
172
-
173
202
const packageManager = detectPackageManager ( workspaceRoot ) ;
174
203
175
204
const originalPackageWorkspaces = await getPackagesInPackageManagerWorkspace (
@@ -186,8 +215,7 @@ export async function importHandler(options: ImportOptions) {
186
215
source ,
187
216
relativeDestination ,
188
217
tempImportBranch ,
189
- sourceRemoteUrl ,
190
- importRemoteName
218
+ sourceRemoteUrl
191
219
) ;
192
220
193
221
await createTemporaryRemote (
@@ -220,22 +248,74 @@ export async function importHandler(options: ImportOptions) {
220
248
options . interactive
221
249
) ;
222
250
223
- if ( plugins . length > 0 ) {
224
- output . log ( { title : 'Installing Plugins' } ) ;
225
- installPlugins ( workspaceRoot , plugins , pmc , updatePackageScripts ) ;
251
+ if ( packageManager !== sourcePackageManager ) {
252
+ output . warn ( {
253
+ title : `Mismatched package managers` ,
254
+ bodyLines : [
255
+ `The source repository is using a different package manager (${ sourcePackageManager } ) than this workspace (${ packageManager } ).` ,
256
+ `This could lead to install issues due to discrepancies in "package.json" features.` ,
257
+ ] ,
258
+ } ) ;
259
+ }
226
260
227
- await destinationGitClient . amendCommit ( ) ;
261
+ // If install fails, we should continue since the errors could be resolved later.
262
+ let installFailed = false ;
263
+ if ( plugins . length > 0 ) {
264
+ try {
265
+ output . log ( { title : 'Installing Plugins' } ) ;
266
+ installPlugins ( workspaceRoot , plugins , pmc , updatePackageScripts ) ;
267
+
268
+ await destinationGitClient . amendCommit ( ) ;
269
+ } catch ( e ) {
270
+ installFailed = true ;
271
+ output . error ( {
272
+ title : `Install failed: ${ e . message || 'Unknown error' } ` ,
273
+ bodyLines : [ e . stack ] ,
274
+ } ) ;
275
+ }
228
276
} else if ( await needsInstall ( packageManager , originalPackageWorkspaces ) ) {
229
- output . log ( {
230
- title : 'Installing dependencies for imported code' ,
231
- } ) ;
277
+ try {
278
+ output . log ( {
279
+ title : 'Installing dependencies for imported code' ,
280
+ } ) ;
281
+
282
+ runInstall ( workspaceRoot , getPackageManagerCommand ( packageManager ) ) ;
283
+
284
+ await destinationGitClient . amendCommit ( ) ;
285
+ } catch ( e ) {
286
+ installFailed = true ;
287
+ output . error ( {
288
+ title : `Install failed: ${ e . message || 'Unknown error' } ` ,
289
+ bodyLines : [ e . stack ] ,
290
+ } ) ;
291
+ }
292
+ }
232
293
233
- runInstall ( workspaceRoot , getPackageManagerCommand ( packageManager ) ) ;
294
+ console . log ( await destinationGitClient . showStat ( ) ) ;
234
295
235
- await destinationGitClient . amendCommit ( ) ;
296
+ if ( installFailed ) {
297
+ const pmc = getPackageManagerCommand ( packageManager ) ;
298
+ output . warn ( {
299
+ title : `The import was successful, but the install failed` ,
300
+ bodyLines : [
301
+ `You may need to run "${ pmc . install } " manually to resolve the issue. The error is logged above.` ,
302
+ ] ,
303
+ } ) ;
236
304
}
237
305
238
- console . log ( await destinationGitClient . showStat ( ) ) ;
306
+ await warnOnMissingWorkspacesEntry ( packageManager , pmc , relativeDestination ) ;
307
+
308
+ // When only a subdirectory is imported, there might be devDependencies in the root package.json file
309
+ // that needs to be ported over as well.
310
+ if ( ref ) {
311
+ output . log ( {
312
+ title : `Check root dependencies` ,
313
+ bodyLines : [
314
+ `"dependencies" and "devDependencies" are not imported from the source repository (${ sourceRemoteUrl } ).` ,
315
+ `You may need to add some of those dependencies to this workspace in order to run tasks successfully.` ,
316
+ ] ,
317
+ } ) ;
318
+ }
239
319
240
320
output . log ( {
241
321
title : `Merging these changes into ${ getBaseRef ( nxJson ) } ` ,
@@ -274,3 +354,77 @@ async function createTemporaryRemote(
274
354
await destinationGitClient . addGitRemote ( remoteName , sourceRemoteUrl ) ;
275
355
await destinationGitClient . fetch ( remoteName ) ;
276
356
}
357
+
358
+ // If the user imports a project that isn't in NPM/Yarn/PNPM workspaces, then its dependencies
359
+ // will not be installed. We should warn users and provide instructions on how to fix this.
360
+ async function warnOnMissingWorkspacesEntry (
361
+ pm : PackageManager ,
362
+ pmc : PackageManagerCommands ,
363
+ pkgPath : string
364
+ ) {
365
+ if ( ! isWorkspacesEnabled ( pm , workspaceRoot ) ) {
366
+ output . warn ( {
367
+ title : `Missing workspaces in package.json` ,
368
+ bodyLines :
369
+ pm === 'npm'
370
+ ? [
371
+ `We recommend enabling NPM workspaces to install dependencies for the imported project.` ,
372
+ `Add \`"workspaces": ["${ pkgPath } "]\` to package.json and run "${ pmc . install } ".` ,
373
+ `See: https://docs.npmjs.com/cli/using-npm/workspaces` ,
374
+ ]
375
+ : pm === 'yarn'
376
+ ? [
377
+ `We recommend enabling Yarn workspaces to install dependencies for the imported project.` ,
378
+ `Add \`"workspaces": ["${ pkgPath } "]\` to package.json and run "${ pmc . install } ".` ,
379
+ `See: https://yarnpkg.com/features/workspaces` ,
380
+ ]
381
+ : pm === 'bun'
382
+ ? [
383
+ `We recommend enabling Bun workspaces to install dependencies for the imported project.` ,
384
+ `Add \`"workspaces": ["${ pkgPath } "]\` to package.json and run "${ pmc . install } ".` ,
385
+ `See: https://bun.sh/docs/install/workspaces` ,
386
+ ]
387
+ : [
388
+ `We recommend enabling PNPM workspaces to install dependencies for the imported project.` ,
389
+ `Add the following entry to to pnpm-workspace.yaml and run "${ pmc . install } ":` ,
390
+ chalk . bold ( `packages:\n - '${ pkgPath } '` ) ,
391
+ `See: https://pnpm.io/workspaces` ,
392
+ ] ,
393
+ } ) ;
394
+ } else {
395
+ // Check if the new package is included in existing workspaces entries. If not, warn the user.
396
+ let workspaces : string [ ] | null = null ;
397
+
398
+ if ( pm === 'npm' || pm === 'yarn' || pm === 'bun' ) {
399
+ const packageJson = readPackageJson ( ) ;
400
+ workspaces = packageJson . workspaces ;
401
+ } else if ( pm === 'pnpm' ) {
402
+ const yamlPath = join ( workspaceRoot , 'pnpm-workspace.yaml' ) ;
403
+ if ( existsSync ( yamlPath ) ) {
404
+ const yamlContent = await fsp . readFile ( yamlPath , 'utf-8' ) ;
405
+ const yaml = yamlLoad ( yamlContent ) ;
406
+ workspaces = yaml . packages ;
407
+ }
408
+ }
409
+
410
+ if ( workspaces ) {
411
+ const isPkgIncluded = workspaces . some ( ( w ) => minimatch ( pkgPath , w ) ) ;
412
+ if ( ! isPkgIncluded ) {
413
+ const pkgsDir = dirname ( pkgPath ) ;
414
+ output . warn ( {
415
+ title : `Project missing in workspaces` ,
416
+ bodyLines :
417
+ pm === 'npm' || pm === 'yarn' || pm === 'bun'
418
+ ? [
419
+ `The imported project (${ pkgPath } ) is missing the "workspaces" field in package.json.` ,
420
+ `Add "${ pkgsDir } /*" to workspaces run "${ pmc . install } ".` ,
421
+ ]
422
+ : [
423
+ `The imported project (${ pkgPath } ) is missing the "packages" field in pnpm-workspaces.yaml.` ,
424
+ `Add "${ pkgsDir } /*" to packages run "${ pmc . install } ".` ,
425
+ ] ,
426
+ } ) ;
427
+ }
428
+ }
429
+ }
430
+ }
0 commit comments