diff --git a/.changeset/silly-rules-join.md b/.changeset/silly-rules-join.md new file mode 100644 index 0000000000..7e4d3281d7 --- /dev/null +++ b/.changeset/silly-rules-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +Initializes a git repository when creating an app using @packages/create-app diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 8e7b3bae7d..95039fe9e7 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -32,6 +32,7 @@ const promptMock = jest.spyOn(inquirer, 'prompt'); const checkPathExistsMock = jest.spyOn(tasks, 'checkPathExistsTask'); const templatingMock = jest.spyOn(tasks, 'templatingTask'); const checkAppExistsMock = jest.spyOn(tasks, 'checkAppExistsTask'); +const initGitRepositoryMock = jest.spyOn(tasks, 'initGitRepository'); const createTemporaryAppFolderMock = jest.spyOn( tasks, 'createTemporaryAppFolderTask', @@ -67,6 +68,7 @@ describe('command entrypoint', () => { await createApp(cmd); expect(checkAppExistsMock).toHaveBeenCalled(); expect(createTemporaryAppFolderMock).toHaveBeenCalled(); + expect(initGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(moveAppMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); @@ -76,6 +78,7 @@ describe('command entrypoint', () => { const cmd = { path: 'myDirectory' } as unknown as Command; await createApp(cmd); expect(checkPathExistsMock).toHaveBeenCalled(); + expect(initGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 7cf589e5f8..22f7424cda 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -28,6 +28,7 @@ import { createTemporaryAppFolderTask, moveAppTask, templatingTask, + initGitRepository, } from './lib/tasks'; export default async (opts: OptionValues): Promise => { @@ -79,6 +80,9 @@ export default async (opts: OptionValues): Promise => { Task.section('Checking that supplied path exists'); await checkPathExistsTask(appDir); + Task.section('Initializing git repository'); + await initGitRepository(appDir, answers); + Task.section('Preparing files'); await templatingTask(templateDir, opts.path, answers); } else { @@ -90,6 +94,9 @@ export default async (opts: OptionValues): Promise => { Task.section('Creating a temporary app directory'); await createTemporaryAppFolderTask(tempDir); + Task.section('Initializing git repository'); + await initGitRepository(tempDir, answers); + Task.section('Preparing files'); await templatingTask(templateDir, tempDir, answers); diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index decc89fbd4..3550fc330a 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -24,6 +24,7 @@ import { checkAppExistsTask, checkPathExistsTask, createTemporaryAppFolderTask, + initGitRepository, moveAppTask, templatingTask, } from './tasks'; @@ -86,28 +87,100 @@ jest.mock('./versions', () => ({ }, })); -describe('tasks', () => { - beforeEach(() => { - mockFs({ - 'projects/my-module.ts': '', - 'projects/dir/my-file.txt': '', - 'tmp/mockApp/.gitignore': '', - 'tmp/mockApp/package.json': '', - 'tmp/mockApp/packages/app/package.json': '', - // load templates into mock filesystem - 'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')), - }); +const mockExec = child_process.exec as unknown as jest.MockedFunction< + ( + command: string, + callback: (error: null, stdout: any, stderr: any) => void, + ) => void +>; + +beforeEach(() => { + mockFs({ + 'projects/my-module.ts': '', + 'projects/dir/my-file.txt': '', + 'tmp/mockApp/.gitignore': '', + 'tmp/mockApp/package.json': '', + 'tmp/mockApp/packages/app/package.json': '', + // load templates into mock filesystem + 'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')), + }); +}); + +afterEach(() => { + mockExec.mockRestore(); + mockFs.restore(); +}); + +describe('checkAppExistsTask', () => { + it('should do nothing if the directory does not exist', async () => { + const dir = 'projects/'; + const name = 'MyNewApp'; + await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow(); }); afterEach(() => { mockFs.restore(); }); - describe('checkAppExistsTask', () => { - it('should do nothing if the directory does not exist', async () => { - const dir = 'projects/'; - const name = 'MyNewApp'; - await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow(); + it('should throw an error when a directory of the same name exists', async () => { + const dir = 'projects/'; + const name = 'dir'; + await expect(checkAppExistsTask(dir, name)).rejects.toThrow( + 'already exists', + ); + }); +}); + +describe('checkPathExistsTask', () => { + it('should create a directory at the given path', async () => { + const appDir = 'projects/newProject'; + await expect(checkPathExistsTask(appDir)).resolves.not.toThrow(); + expect(fs.existsSync(appDir)).toBe(true); + }); + + it('should do nothing if a directory of the same name exists', async () => { + const appDir = 'projects/dir'; + await expect(checkPathExistsTask(appDir)).resolves.not.toThrow(); + expect(fs.existsSync(appDir)).toBe(true); + }); + + it('should fail if a file of the same name exists', async () => { + await expect(checkPathExistsTask('projects/my-module.ts')).rejects.toThrow( + 'already exists', + ); + }); +}); + +describe('createTemporaryAppFolderTask', () => { + it('should create a directory at a given path', async () => { + const tempDir = 'projects/tmpFolder'; + await expect(createTemporaryAppFolderTask(tempDir)).resolves.not.toThrow(); + expect(fs.existsSync(tempDir)).toBe(true); + }); + + it('should fail if a directory of the same name exists', async () => { + const tempDir = 'projects/dir'; + await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow( + 'file already exists', + ); + }); + + it('should fail if a file of the same name exists', async () => { + const tempDir = 'projects/dir/my-file.txt'; + await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow( + 'file already exists', + ); + }); +}); + +describe('buildAppTask', () => { + it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => { + const mockChdir = jest.spyOn(process, 'chdir'); + + // requires callback implementation to support `promisify` wrapper + // https://stackoverflow.com/a/60579617/10044859 + mockExec.mockImplementation((_command, callback) => { + callback(null, 'standard out', 'standard error'); }); it('should throw an error when a file of the same name exists', async () => { @@ -274,3 +347,36 @@ describe('tasks', () => { }); }); }); + +describe('initGitRepository', () => { + it('should initialize a git repository at the given path', async () => { + const destinationDir = 'tmp/mockApp/'; + const context = { + defaultBranch: '', + }; + + mockExec.mockImplementation((_command, callback) => { + callback(null, { stdout: 'main' }, 'standard error'); + }); + + await initGitRepository(destinationDir, context); + + expect(context.defaultBranch).toBe('main'); + expect(mockExec).toHaveBeenCalledTimes(3); + expect(mockExec).toHaveBeenNthCalledWith( + 1, + 'git init', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 2, + 'git commit --allow-empty -m "Initial commit"', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 3, + 'git branch --format="%(refname:short)"', + expect.any(Function), + ); + }); +}); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 1650432969..71575bbd4f 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -236,3 +236,34 @@ export async function moveAppTask( }); }); } + +/** + * Initializes a git repository in the destination folder + * + * @param dir - source path to initialize git repository in + * @param context - template parameters + * @throws if `exec` fails + */ +export async function initGitRepository(dir: string, context: any) { + const runCmd = async (cmd: string) => { + let cmdResponse = { stdout: '', stderr: '' }; + await Task.forItem('executing', cmd, async () => { + process.chdir(dir); + cmdResponse = await exec(cmd).catch(error => { + process.stdout.write(error.stderr); + process.stdout.write(error.stdout); + throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); + }); + }); + return cmdResponse; + }; + + await runCmd('git init'); + await runCmd('git commit --allow-empty -m "Initial commit"'); + + const defaultBranch = await runCmd('git branch --format="%(refname:short)"'); + + context.defaultBranch = defaultBranch.stdout + ? defaultBranch.stdout.trim() + : 'master'; +} diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index dd4d937db0..19cb98ec09 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -14,9 +14,9 @@ "tsc": "tsc", "tsc:full": "tsc --skipLibCheck false --incremental false", "clean": "backstage-cli repo clean", - "test": "backstage-cli repo test", - "test:all": "backstage-cli repo test --coverage", - "lint": "backstage-cli repo lint --since origin/master", + "test": "backstage-cli test", + "test:all": "lerna run test -- --coverage", + "lint": "backstage-cli repo lint --since origin/{{defaultBranch}}", "lint:all": "backstage-cli repo lint", "prettier:check": "prettier --check .", "create-plugin": "backstage-cli create-plugin --scope internal",