Skip to content

Commit c19a8e8

Browse files
cogwirrelclaude
andauthored
fix(misc): allow create-nx-workspace . --no-interactive in empty directory (#35281)
Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent ac7afa3 commit c19a8e8

2 files changed

Lines changed: 135 additions & 4 deletions

File tree

packages/create-nx-workspace/bin/create-nx-workspace.spec.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
validateWorkspaceName,
33
resolveSpecialFolderName,
4+
determineFolder,
45
} from './create-nx-workspace';
56
import { CnwError } from '../src/utils/error-utils';
67
import { mkdtempSync, mkdirSync, rmSync, realpathSync } from 'fs';
@@ -43,6 +44,110 @@ describe('validateWorkspaceName', () => {
4344
});
4445
});
4546

47+
describe('determineFolder', () => {
48+
let originalCwd: string;
49+
50+
beforeEach(() => {
51+
originalCwd = process.cwd();
52+
});
53+
54+
afterEach(() => {
55+
process.chdir(originalCwd);
56+
});
57+
58+
function makeParsedArgs(
59+
overrides: Partial<{
60+
name: string;
61+
positional: string;
62+
interactive: boolean;
63+
}> = {}
64+
) {
65+
return {
66+
_: overrides.positional ? [overrides.positional] : [],
67+
$0: 'create-nx-workspace',
68+
name: overrides.name ?? '',
69+
interactive: overrides.interactive ?? false,
70+
} as any;
71+
}
72+
73+
it('should return directory basename for "." in non-interactive mode', async () => {
74+
const tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'cnw-test-')));
75+
process.chdir(tmpDir);
76+
77+
const parsedArgs = makeParsedArgs({ positional: '.', interactive: false });
78+
const result = await determineFolder(parsedArgs);
79+
80+
expect(result).toBe(basename(tmpDir));
81+
expect(parsedArgs.workingDir).toBe(dirname(tmpDir));
82+
83+
rmSync(tmpDir, { recursive: true });
84+
});
85+
86+
it('should return directory basename for "./" in non-interactive mode', async () => {
87+
const tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'cnw-test-')));
88+
process.chdir(tmpDir);
89+
90+
const parsedArgs = makeParsedArgs({ positional: './', interactive: false });
91+
const result = await determineFolder(parsedArgs);
92+
93+
expect(result).toBe(basename(tmpDir));
94+
expect(parsedArgs.workingDir).toBe(dirname(tmpDir));
95+
96+
rmSync(tmpDir, { recursive: true });
97+
});
98+
99+
it('should return directory basename for "." in interactive mode', async () => {
100+
const tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'cnw-test-')));
101+
process.chdir(tmpDir);
102+
103+
const parsedArgs = makeParsedArgs({ positional: '.', interactive: true });
104+
const result = await determineFolder(parsedArgs);
105+
106+
expect(result).toBe(basename(tmpDir));
107+
108+
rmSync(tmpDir, { recursive: true });
109+
});
110+
111+
it('should default to directory basename when no name given in non-interactive mode', async () => {
112+
const tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'cnw-test-')));
113+
process.chdir(tmpDir);
114+
115+
const parsedArgs = makeParsedArgs({ interactive: false });
116+
const result = await determineFolder(parsedArgs);
117+
118+
expect(result).toBe(basename(tmpDir));
119+
120+
rmSync(tmpDir, { recursive: true });
121+
});
122+
123+
it('should return the name directly when it does not exist as a directory', async () => {
124+
const parsedArgs = makeParsedArgs({
125+
positional: 'nonexistent-workspace-name',
126+
interactive: false,
127+
});
128+
const result = await determineFolder(parsedArgs);
129+
130+
expect(result).toBe('nonexistent-workspace-name');
131+
});
132+
133+
it('should throw DIRECTORY_EXISTS for an existing directory name in non-interactive mode', async () => {
134+
const tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'cnw-test-')));
135+
const existing = join(tmpDir, 'existing');
136+
mkdirSync(existing);
137+
process.chdir(tmpDir);
138+
139+
const parsedArgs = makeParsedArgs({
140+
positional: 'existing',
141+
interactive: false,
142+
});
143+
144+
await expect(determineFolder(parsedArgs)).rejects.toThrow(CnwError);
145+
await expect(determineFolder(parsedArgs)).rejects.toThrow(/already exists/);
146+
147+
rmSync(tmpDir, { recursive: true });
148+
});
149+
});
150+
46151
describe('resolveSpecialFolderName', () => {
47152
let originalCwd: string;
48153

packages/create-nx-workspace/bin/create-nx-workspace.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,11 @@ export function validateWorkspaceName(name: string): void {
833833
}
834834
}
835835

836+
/** Returns `true` when the folder name refers to the current directory. */
837+
function isCurrentDirReference(folderName: string): boolean {
838+
return folderName === '.' || folderName === './';
839+
}
840+
836841
/**
837842
* Resolves special folder name patterns (`.`, `./`, absolute paths) into a
838843
* workspace name and a `workingDir` override so that downstream functions
@@ -846,8 +851,8 @@ export function validateWorkspaceName(name: string): void {
846851
export function resolveSpecialFolderName(
847852
folderName: string
848853
): { name: string; workingDir: string } | null {
849-
// Handle "." and "./" — user wants to init in the current directory
850-
if (folderName === '.' || folderName === './') {
854+
// User wants to init in the current directory
855+
if (isCurrentDirReference(folderName)) {
851856
const cwd = resolve(process.cwd());
852857
if (readdirSync(cwd).length > 0) {
853858
throw new CnwError(
@@ -876,7 +881,12 @@ export function resolveSpecialFolderName(
876881
return null;
877882
}
878883

879-
async function determineFolder(
884+
/**
885+
* Determines the folder name for the new workspace.
886+
*
887+
* @visibleForTesting
888+
*/
889+
export async function determineFolder(
880890
parsedArgs: yargs.Arguments<Arguments>
881891
): Promise<string> {
882892
const rawFolderName: string = parsedArgs._[0]
@@ -893,8 +903,15 @@ async function determineFolder(
893903

894904
validateWorkspaceName(folderName);
895905

906+
// When input is "." or "./", resolveSpecialFolderName already validated
907+
// the directory is empty. The target always "exists" because it IS the
908+
// current working directory, so skip the existsSync check and default
909+
// the workspace name to the directory name.
910+
if (isCurrentDirReference(rawFolderName)) {
911+
return folderName;
912+
}
913+
896914
// If directory exists, either re-prompt (interactive) or error (non-interactive)
897-
// Check relative to workingDir when set (e.g. absolute path resolved to a different parent)
898915
const targetDir = resolved?.workingDir
899916
? join(resolved.workingDir, folderName)
900917
: folderName;
@@ -914,6 +931,15 @@ async function determineFolder(
914931
return folderName;
915932
}
916933

934+
// When non-interactive and no name is provided, default to the current
935+
// directory name instead of prompting.
936+
if (!parsedArgs.interactive || isCI()) {
937+
const cwd = resolve(process.cwd());
938+
const folderName = basename(cwd);
939+
validateWorkspaceName(folderName);
940+
return folderName;
941+
}
942+
917943
return promptForFolder(parsedArgs);
918944
}
919945

0 commit comments

Comments
 (0)