diff --git a/__e2e__/init.test.ts b/__e2e__/init.test.ts index fec60dbce..96aaabe1e 100644 --- a/__e2e__/init.test.ts +++ b/__e2e__/init.test.ts @@ -39,12 +39,38 @@ afterEach(() => { cleanupSync(DIR); }); -test('init fails if the directory already exists', () => { +test('init passes if the directory already exists', () => { fs.mkdirSync(path.join(DIR, 'TestInit')); - const {stderr} = runCLI(DIR, ['init', 'TestInit'], {expectedFailure: true}); + const {stdout} = runCLI(DIR, ['init', 'TestInit']); + + expect(stdout).toContain('Run instructions'); +}); + +test('init fails if the directory contains conflicting files/sub-directories', () => { + const projectName = 'TestInit'; + const directoryName = 'custom-path'; + const targetDirectory = path.resolve(DIR, directoryName); + // pre existing directory structure + writeFiles(targetDirectory, { + 'package.json': '', + 'package-lock.json': '', + 'yarn.lock': '', + 'babel.config.js': '', + 'android/gradlew': '', + [`ios/${projectName}/AppDelegate.h`]: '', + }); + + const {stderr} = runCLI( + DIR, + ['init', '--directory', directoryName, projectName], + {expectedFailure: true}, + ); + + expect(stderr).not.toContain('ios'); + expect(stderr).toContain('package.json'); expect(stderr).toContain( - 'error Cannot initialize new project because directory "TestInit" already exists.', + 'Either try using a new directory name, or remove the files listed above.', ); }); diff --git a/packages/cli/src/commands/init/constants.ts b/packages/cli/src/commands/init/constants.ts new file mode 100644 index 000000000..ae14c23d1 --- /dev/null +++ b/packages/cli/src/commands/init/constants.ts @@ -0,0 +1,14 @@ +export const UNDERSCORED_DOTFILES = [ + 'buckconfig', + 'eslintrc.js', + 'flowconfig', + 'gitattributes', + 'gitignore', + 'prettierrc.js', + 'watchmanconfig', + 'editorconfig', + 'bundle', + 'ruby-version', + 'node-version', + 'xcode.env', +]; diff --git a/packages/cli/src/commands/init/editTemplate.ts b/packages/cli/src/commands/init/editTemplate.ts index ed1321956..5e456125f 100644 --- a/packages/cli/src/commands/init/editTemplate.ts +++ b/packages/cli/src/commands/init/editTemplate.ts @@ -6,6 +6,7 @@ import walk from '../../tools/walk'; // `gracefulify` does not support patching `fs.promises`. Use `fs-extra`, which // exposes its own promise-based interface over `graceful-fs`. import fs from 'fs-extra'; +import {UNDERSCORED_DOTFILES} from './constants'; interface PlaceholderConfig { projectName: string; @@ -58,21 +59,6 @@ function shouldIgnoreFile(filePath: string) { return filePath.match(/node_modules|yarn.lock|package-lock.json/g); } -const UNDERSCORED_DOTFILES = [ - 'buckconfig', - 'eslintrc.js', - 'flowconfig', - 'gitattributes', - 'gitignore', - 'prettierrc.js', - 'watchmanconfig', - 'editorconfig', - 'bundle', - 'ruby-version', - 'node-version', - 'xcode.env', -]; - async function processDotfiles(filePath: string) { const dotfile = UNDERSCORED_DOTFILES.find((e) => filePath.includes(`_${e}`)); diff --git a/packages/cli/src/commands/init/errors/ConflictingFilesError.ts b/packages/cli/src/commands/init/errors/ConflictingFilesError.ts new file mode 100644 index 000000000..82b62253e --- /dev/null +++ b/packages/cli/src/commands/init/errors/ConflictingFilesError.ts @@ -0,0 +1,14 @@ +export default class ConflictingFilesError extends Error { + constructor(directoryName: string, files: string[]) { + let errorString = ''; + + errorString += `The directory ${directoryName} contains files that could conflict:\n`; + for (const file of files) { + errorString += `- ${file}\n`; + } + errorString += + 'Either try using a new directory name, or remove the files listed above.'; + + super(errorString); + } +} diff --git a/packages/cli/src/commands/init/init.ts b/packages/cli/src/commands/init/init.ts index 7d7e2e2d8..cdd041c9a 100644 --- a/packages/cli/src/commands/init/init.ts +++ b/packages/cli/src/commands/init/init.ts @@ -2,7 +2,6 @@ import os from 'os'; import path from 'path'; import fs from 'fs-extra'; import {validateProjectName} from './validate'; -import DirectoryAlreadyExistsError from './errors/DirectoryAlreadyExistsError'; import printRunInstructions from './printRunInstructions'; import { CLIError, @@ -20,6 +19,9 @@ import {changePlaceholderInTemplate} from './editTemplate'; import * as PackageManager from '../../tools/packageManager'; import {installPods} from '@react-native-community/cli-doctor'; import banner from './banner'; +import ConflictingFilesError from './errors/ConflictingFilesError'; +import walk from '../../tools/walk'; +import {UNDERSCORED_DOTFILES} from './constants'; import TemplateAndVersionError from './errors/TemplateAndVersionError'; const DEFAULT_VERSION = 'latest'; @@ -47,22 +49,62 @@ function doesDirectoryExist(dir: string) { return fs.existsSync(dir); } -async function setProjectDirectory(directory: string) { - if (doesDirectoryExist(directory)) { - throw new DirectoryAlreadyExistsError(directory); - } +function getDirectoryFilesRecursive(dir: string): string[] { + return walk(dir, true) + .map((file) => file.replace(dir, '')) + .filter(Boolean); +} - try { - fs.mkdirSync(directory, {recursive: true}); - process.chdir(directory); - } catch (error) { - throw new CLIError( - 'Error occurred while trying to create project directory.', - error, +function checkProjectDirectoryForConflictsWithTemplate( + projectDirectory: string, + templateName: string, + templateDir: string, + templateSourceDir: string, +) { + const projectDirectoryFiles = getDirectoryFilesRecursive(projectDirectory); + + const templatePath = path.resolve( + templateSourceDir, + 'node_modules', + templateName, + templateDir, + ); + const templateFiles: string[] = []; + getDirectoryFilesRecursive(templatePath).forEach((fileName) => { + templateFiles.push(fileName); + + // We first copy template into project folder, and then we rename underscored files, + // so here we are taking both options into account + const dotfile = UNDERSCORED_DOTFILES.find((e) => + fileName.includes(`_${e}`), ); + if (dotfile) { + templateFiles.push(dotfile); + } + }); + + const conflictingFiles: string[] = projectDirectoryFiles.filter((fileName) => + templateFiles.includes(fileName), + ); + if (conflictingFiles.length > 0) { + throw new ConflictingFilesError(projectDirectory, conflictingFiles); } +} - return process.cwd(); +async function setProjectDirectory(directory: string) { + const projectDirectory = path.join(process.cwd(), directory); + if (!doesDirectoryExist(projectDirectory)) { + try { + fs.mkdirSync(directory, {recursive: true}); + } catch (error) { + throw new CLIError( + 'Error occurred while trying to create project directory.', + error, + ); + } + } + process.chdir(projectDirectory); + return projectDirectory; } function getTemplateName(cwd: string) { @@ -104,6 +146,14 @@ async function createFromTemplate({ const templateName = getTemplateName(templateSourceDir); const templateConfig = getTemplateConfig(templateName, templateSourceDir); + + await checkProjectDirectoryForConflictsWithTemplate( + projectDirectory, + templateName, + templateConfig.templateDir, + templateSourceDir, + ); + await copyTemplate( templateName, templateConfig.templateDir, diff --git a/packages/cli/src/tools/walk.ts b/packages/cli/src/tools/walk.ts index 5cc2ea6f1..cd98a001e 100644 --- a/packages/cli/src/tools/walk.ts +++ b/packages/cli/src/tools/walk.ts @@ -9,16 +9,16 @@ import fs from 'fs'; import path from 'path'; -function walk(current: string): string[] { +function walk(current: string, filesOnly: boolean = false): string[] { if (!fs.lstatSync(current).isDirectory()) { return [current]; } const files = fs .readdirSync(current) - .map((child) => walk(path.join(current, child))); + .map((child) => walk(path.join(current, child), filesOnly)); const result: string[] = []; - return result.concat.apply([current], files); + return result.concat.apply(!filesOnly ? [current] : [], files); } export default walk;