Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: feat: Init for currently exising folders #1591

32 changes: 29 additions & 3 deletions __e2e__/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
);
});

Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/commands/init/constants.ts
Original file line number Diff line number Diff line change
@@ -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',
];
16 changes: 1 addition & 15 deletions packages/cli/src/commands/init/editTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}`));

Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/commands/init/errors/ConflictingFilesError.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
76 changes: 63 additions & 13 deletions packages/cli/src/commands/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/tools/walk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;