@form8ion/project 21.0.1 → 21.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/index.js +75 -67
  2. package/lib/index.js.map +1 -1
  3. package/package.json +17 -14
  4. package/src/contributing/index.js +1 -0
  5. package/src/contributing/scaffolder.js +17 -0
  6. package/src/contributing/scaffolder.test.js +22 -0
  7. package/src/dependency-updater/prompt.js +11 -0
  8. package/src/dependency-updater/prompt.test.js +30 -0
  9. package/src/dependency-updater/scaffolder.js +14 -0
  10. package/src/dependency-updater/scaffolder.test.js +42 -0
  11. package/src/dependency-updater/schema.js +4 -0
  12. package/src/dependency-updater/schema.test.js +43 -0
  13. package/src/editorconfig/index.js +1 -0
  14. package/src/editorconfig/scaffolder.js +7 -0
  15. package/src/editorconfig/scaffolder.test.js +26 -0
  16. package/src/index.js +9 -0
  17. package/src/language/index.js +2 -0
  18. package/src/language/prompt.js +12 -0
  19. package/src/language/prompt.test.js +30 -0
  20. package/src/language/scaffolder.js +12 -0
  21. package/src/language/scaffolder.test.js +32 -0
  22. package/src/language/schema.js +4 -0
  23. package/src/language/schema.test.js +43 -0
  24. package/src/license/index.js +3 -0
  25. package/src/license/lifter.js +19 -0
  26. package/src/license/lifter.test.js +30 -0
  27. package/src/license/scaffolder.js +24 -0
  28. package/src/license/scaffolder.test.js +60 -0
  29. package/src/license/tester.js +5 -0
  30. package/src/license/tester.test.js +25 -0
  31. package/src/lift.js +18 -0
  32. package/src/lift.test.js +40 -0
  33. package/src/options-schemas.js +3 -0
  34. package/src/options-schemas.test.js +20 -0
  35. package/src/options-validator.js +18 -0
  36. package/src/options-validator.test.js +50 -0
  37. package/src/prompts/conditionals.js +13 -0
  38. package/src/prompts/conditionals.test.js +51 -0
  39. package/src/prompts/question-names.js +7 -0
  40. package/src/prompts/questions.js +6 -0
  41. package/src/prompts/questions.test.js +25 -0
  42. package/src/prompts/terminal-prompt.js +5 -0
  43. package/src/prompts/terminal-prompt.test.js +20 -0
  44. package/src/scaffolder.js +76 -0
  45. package/src/scaffolder.test.js +297 -0
  46. package/src/template-path.js +8 -0
  47. package/src/template-path.test.js +14 -0
  48. package/src/vcs/git/index.js +1 -0
  49. package/src/vcs/git/remotes.js +51 -0
  50. package/src/vcs/git/remotes.test.js +86 -0
  51. package/src/vcs/host/index.js +1 -0
  52. package/src/vcs/host/prompt.js +15 -0
  53. package/src/vcs/host/prompt.test.js +47 -0
  54. package/src/vcs/host/scaffolder.js +16 -0
  55. package/src/vcs/host/scaffolder.test.js +41 -0
  56. package/src/vcs/host/schema.js +4 -0
  57. package/src/vcs/host/schema.test.js +46 -0
  58. package/src/vcs/index.js +1 -0
  59. package/src/vcs/prompt.js +17 -0
  60. package/src/vcs/prompt.test.js +27 -0
  61. package/src/vcs/scaffolder.js +34 -0
  62. package/src/vcs/scaffolder.test.js +60 -0
@@ -0,0 +1,76 @@
1
+ import deepmerge from 'deepmerge';
2
+ import {execa} from 'execa';
3
+ import {questionNames as coreQuestionNames} from '@form8ion/core';
4
+ import {reportResults} from '@form8ion/results-reporter';
5
+ import {scaffold as scaffoldReadme} from '@form8ion/readme';
6
+ import {info} from '@travi/cli-messages';
7
+
8
+ import {scaffold as scaffoldLanguage} from './language/index.js';
9
+ import {scaffold as scaffoldVcs} from './vcs/index.js';
10
+ import {scaffold as scaffoldLicense} from './license/index.js';
11
+ import scaffoldDependencyUpdater from './dependency-updater/scaffolder.js';
12
+ import {promptForBaseDetails} from './prompts/questions.js';
13
+ import {validate} from './options-validator.js';
14
+ import {scaffold as scaffoldEditorConfig} from './editorconfig/index.js';
15
+ import {scaffold as scaffoldContributing} from './contributing/index.js';
16
+ import lift from './lift.js';
17
+
18
+ export async function scaffold(options) {
19
+ const projectRoot = process.cwd();
20
+ const {decisions, plugins: {dependencyUpdaters, languages, vcsHosts = {}}} = validate(options);
21
+
22
+ const {
23
+ [coreQuestionNames.PROJECT_NAME]: projectName,
24
+ [coreQuestionNames.LICENSE]: chosenLicense,
25
+ [coreQuestionNames.VISIBILITY]: visibility,
26
+ [coreQuestionNames.DESCRIPTION]: description,
27
+ [coreQuestionNames.COPYRIGHT_YEAR]: copyrightYear,
28
+ [coreQuestionNames.COPYRIGHT_HOLDER]: copyHolder
29
+ } = await promptForBaseDetails(projectRoot, decisions);
30
+ const copyright = {year: copyrightYear, holder: copyHolder};
31
+
32
+ const [vcsResults, contributing, license] = await Promise.all([
33
+ scaffoldVcs({projectRoot, projectName, decisions, vcsHosts, visibility, description}),
34
+ scaffoldContributing({visibility}),
35
+ scaffoldLicense({projectRoot, license: chosenLicense, copyright}),
36
+ scaffoldReadme({projectName, projectRoot, description}),
37
+ scaffoldEditorConfig({projectRoot})
38
+ ]);
39
+
40
+ const dependencyUpdaterResults = vcsResults.vcs && await scaffoldDependencyUpdater(
41
+ dependencyUpdaters,
42
+ decisions,
43
+ {projectRoot, vcs: vcsResults.vcs}
44
+ );
45
+
46
+ const language = await scaffoldLanguage(
47
+ languages,
48
+ decisions,
49
+ {projectRoot, projectName, vcs: vcsResults.vcs, visibility, license: chosenLicense || 'UNLICENSED', description}
50
+ );
51
+
52
+ const mergedResults = deepmerge.all([
53
+ license,
54
+ language,
55
+ dependencyUpdaterResults,
56
+ contributing,
57
+ vcsResults
58
+ ].filter(Boolean));
59
+
60
+ await lift({
61
+ projectRoot,
62
+ vcs: vcsResults.vcs,
63
+ results: mergedResults,
64
+ enhancers: {...dependencyUpdaters, ...languages, ...vcsHosts}
65
+ });
66
+
67
+ if (language && language.verificationCommand) {
68
+ info('Verifying the generated project');
69
+
70
+ const subprocess = execa(language.verificationCommand, {shell: true});
71
+ subprocess.stdout.pipe(process.stdout);
72
+ await subprocess;
73
+ }
74
+
75
+ reportResults(mergedResults);
76
+ }
@@ -0,0 +1,297 @@
1
+ import deepmerge from 'deepmerge';
2
+ import {execa} from 'execa';
3
+ import {questionNames as coreQuestionNames} from '@form8ion/core';
4
+ import {scaffold as scaffoldReadme} from '@form8ion/readme';
5
+ import * as resultsReporter from '@form8ion/results-reporter';
6
+
7
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
8
+ import any from '@travi/any';
9
+ import {when} from 'vitest-when';
10
+
11
+ import {scaffold as scaffoldVcs} from './vcs/index.js';
12
+ import * as licenseScaffolder from './license/scaffolder.js';
13
+ import scaffoldLanguage from './language/scaffolder.js';
14
+ import * as dependencyUpdaterScaffolder from './dependency-updater/scaffolder.js';
15
+ import * as optionsValidator from './options-validator.js';
16
+ import * as prompts from './prompts/questions.js';
17
+ import {questionNames} from './prompts/question-names.js';
18
+ import {scaffold as scaffoldEditorconfig} from './editorconfig/index.js';
19
+ import {scaffold as scaffoldContributing} from './contributing/index.js';
20
+ import lift from './lift.js';
21
+ import {scaffold} from './scaffolder.js';
22
+
23
+ vi.mock('execa');
24
+ vi.mock('@form8ion/readme');
25
+ vi.mock('@form8ion/results-reporter');
26
+ vi.mock('./readme');
27
+ vi.mock('./vcs/index.js');
28
+ vi.mock('./license/scaffolder');
29
+ vi.mock('./language/scaffolder');
30
+ vi.mock('./dependency-updater/scaffolder');
31
+ vi.mock('./options-validator');
32
+ vi.mock('./prompts/questions');
33
+ vi.mock('./editorconfig');
34
+ vi.mock('./contributing');
35
+ vi.mock('./lift.js');
36
+
37
+ describe('project scaffolder', () => {
38
+ const originalProcessCwd = process.cwd;
39
+ const options = any.simpleObject();
40
+ const projectPath = any.string();
41
+ const projectName = any.string();
42
+ const description = any.string();
43
+ const homepage = any.url();
44
+ const license = any.string();
45
+ const licenseBadge = any.url();
46
+ const languages = any.simpleObject();
47
+ const vcsHosts = any.simpleObject();
48
+ const documentation = any.simpleObject();
49
+ const vcs = any.simpleObject();
50
+ const gitNextSteps = any.listOf(any.simpleObject);
51
+ const vcsResults = {...any.simpleObject(), vcs, nextSteps: gitNextSteps};
52
+ const tags = any.listOf(any.word);
53
+ const visibility = any.word();
54
+ const vcsIgnore = any.simpleObject();
55
+ const decisions = any.simpleObject();
56
+
57
+ beforeEach(() => {
58
+ process.cwd = vi.fn();
59
+ process.cwd.mockReturnValue(projectPath);
60
+ });
61
+
62
+ afterEach(() => {
63
+ vi.clearAllMocks();
64
+
65
+ process.cwd = originalProcessCwd;
66
+ });
67
+
68
+ it('should generate the project files', async () => {
69
+ const ciBadge = any.url();
70
+ const year = any.word();
71
+ const holder = any.sentence();
72
+ const copyright = {year, holder};
73
+ const dependencyUpdaters = any.simpleObject();
74
+ const dependencyUpdaterNextSteps = any.listOf(any.simpleObject);
75
+ const dependencyUpdaterContributionBadges = any.simpleObject();
76
+ const dependencyUpdaterResults = {
77
+ badges: {contribution: dependencyUpdaterContributionBadges},
78
+ nextSteps: dependencyUpdaterNextSteps
79
+ };
80
+ const languageResults = {
81
+ badges: {status: {ci: ciBadge}},
82
+ vcsIgnore,
83
+ projectDetails: {},
84
+ documentation,
85
+ tags
86
+ };
87
+ const licenseResults = {badges: {consumer: {license: licenseBadge}}};
88
+ const contributingResults = any.simpleObject();
89
+ const mergedResults = deepmerge.all([
90
+ licenseResults,
91
+ languageResults,
92
+ dependencyUpdaterResults,
93
+ contributingResults,
94
+ vcsResults
95
+ ]);
96
+ when(optionsValidator.validate)
97
+ .calledWith(options)
98
+ .thenReturn({decisions, plugins: {dependencyUpdaters, languages, vcsHosts}});
99
+ when(prompts.promptForBaseDetails)
100
+ .calledWith(projectPath, decisions)
101
+ .thenResolve({
102
+ [coreQuestionNames.PROJECT_NAME]: projectName,
103
+ [coreQuestionNames.LICENSE]: license,
104
+ [coreQuestionNames.DESCRIPTION]: description,
105
+ [coreQuestionNames.COPYRIGHT_HOLDER]: holder,
106
+ [coreQuestionNames.COPYRIGHT_YEAR]: year,
107
+ [coreQuestionNames.VISIBILITY]: visibility
108
+ });
109
+ when(scaffoldVcs)
110
+ .calledWith({projectRoot: projectPath, projectName, decisions, vcsHosts, visibility, description})
111
+ .thenResolve(vcsResults);
112
+ when(licenseScaffolder.default)
113
+ .calledWith({projectRoot: projectPath, license, copyright})
114
+ .thenResolve(licenseResults);
115
+ scaffoldLanguage.mockResolvedValue(languageResults);
116
+ when(dependencyUpdaterScaffolder.default)
117
+ .calledWith(dependencyUpdaters, decisions, {projectRoot: projectPath, vcs})
118
+ .thenResolve(dependencyUpdaterResults);
119
+ when(scaffoldContributing).calledWith({visibility}).thenReturn(contributingResults);
120
+
121
+ await scaffold(options);
122
+
123
+ expect(scaffoldReadme).toHaveBeenCalledWith({projectName, projectRoot: projectPath, description});
124
+ expect(dependencyUpdaterScaffolder.default).toHaveBeenCalledWith(
125
+ dependencyUpdaters,
126
+ decisions,
127
+ {projectRoot: projectPath, vcs}
128
+ );
129
+ expect(scaffoldEditorconfig).toHaveBeenCalledWith({projectRoot: projectPath});
130
+ expect(lift).toHaveBeenCalledWith({
131
+ projectRoot: projectPath,
132
+ vcs,
133
+ results: mergedResults,
134
+ enhancers: {...dependencyUpdaters, ...vcsHosts, ...languages}
135
+ });
136
+ expect(resultsReporter.reportResults).toHaveBeenCalledWith(mergedResults);
137
+ });
138
+
139
+ it('should pass the lists of badges from contributors to the readme', async () => {
140
+ const contributingBadges = {
141
+ consumer: any.simpleObject(),
142
+ status: any.simpleObject(),
143
+ contribution: any.simpleObject()
144
+ };
145
+ const languageBadges = {
146
+ consumer: any.simpleObject(),
147
+ status: any.simpleObject(),
148
+ contribution: any.simpleObject()
149
+ };
150
+ const dependencyUpdaterBadges = {
151
+ consumer: any.simpleObject(),
152
+ status: any.simpleObject(),
153
+ contribution: any.simpleObject()
154
+ };
155
+ const licenseBadges = {
156
+ consumer: any.simpleObject(),
157
+ status: any.simpleObject(),
158
+ contribution: any.simpleObject()
159
+ };
160
+ const languageResults = {badges: languageBadges, vcsIgnore, documentation};
161
+ when(optionsValidator.validate).calledWith(options).thenReturn({plugins: {vcsHosts}});
162
+ when(prompts.promptForBaseDetails)
163
+ .calledWith(projectPath, undefined)
164
+ .thenResolve({
165
+ [coreQuestionNames.DESCRIPTION]: description,
166
+ [questionNames.GIT_REPO]: true,
167
+ [coreQuestionNames.PROJECT_NAME]: projectName,
168
+ [coreQuestionNames.VISIBILITY]: visibility
169
+ });
170
+ when(scaffoldContributing).calledWith({visibility}).thenReturn({badges: contributingBadges});
171
+ scaffoldLanguage.mockResolvedValue(languageResults);
172
+ dependencyUpdaterScaffolder.default.mockResolvedValue({badges: dependencyUpdaterBadges});
173
+ licenseScaffolder.default.mockResolvedValue({badges: licenseBadges});
174
+ scaffoldVcs.mockResolvedValue(vcsResults);
175
+
176
+ await scaffold(options);
177
+
178
+ expect(scaffoldReadme).toHaveBeenCalledWith({projectName, projectRoot: projectPath, description});
179
+ });
180
+
181
+ it('should not scaffold the git repo if not requested', async () => {
182
+ when(optionsValidator.validate).calledWith(options).thenReturn({plugins: {}});
183
+ prompts.promptForBaseDetails.mockResolvedValue({[questionNames.GIT_REPO]: false});
184
+ scaffoldReadme.mockResolvedValue();
185
+ scaffoldVcs.mockResolvedValue({});
186
+
187
+ await scaffold(options);
188
+
189
+ expect(dependencyUpdaterScaffolder.default).not.toHaveBeenCalled();
190
+ });
191
+
192
+ it('should scaffold the details of the chosen language plugin', async () => {
193
+ const languageConsumerBadges = any.simpleObject();
194
+ const languageContributionBadges = any.simpleObject();
195
+ const languageStatusBadges = any.simpleObject();
196
+ const languageNextSteps = any.listOf(any.simpleObject);
197
+ const verificationCommand = any.string();
198
+ const execaPipe = vi.fn();
199
+ const languageResults = {
200
+ vcsIgnore,
201
+ badges: {
202
+ consumer: languageConsumerBadges,
203
+ contribution: languageContributionBadges,
204
+ status: languageStatusBadges
205
+ },
206
+ documentation,
207
+ verificationCommand,
208
+ projectDetails: {homepage},
209
+ nextSteps: languageNextSteps,
210
+ tags
211
+ };
212
+ when(optionsValidator.validate)
213
+ .calledWith(options)
214
+ .thenReturn({decisions, plugins: {languages, vcsHosts}});
215
+ scaffoldVcs.mockResolvedValue(vcsResults);
216
+ prompts.promptForBaseDetails.mockResolvedValue({
217
+ [coreQuestionNames.PROJECT_NAME]: projectName,
218
+ [coreQuestionNames.VISIBILITY]: visibility,
219
+ [questionNames.GIT_REPO]: true,
220
+ [coreQuestionNames.LICENSE]: license,
221
+ [coreQuestionNames.DESCRIPTION]: description
222
+ });
223
+ when(scaffoldLanguage).calledWith(languages, decisions, {
224
+ projectName,
225
+ projectRoot: projectPath,
226
+ visibility,
227
+ license,
228
+ vcs,
229
+ description
230
+ }).thenResolve(languageResults);
231
+ when(execa).calledWith(verificationCommand, {shell: true}).thenReturn({stdout: {pipe: execaPipe}});
232
+ dependencyUpdaterScaffolder.default.mockResolvedValue({});
233
+ licenseScaffolder.default.mockResolvedValue({});
234
+ scaffoldContributing.mockResolvedValue({});
235
+
236
+ await scaffold(options);
237
+
238
+ expect(scaffoldReadme).toHaveBeenCalledWith({projectName, projectRoot: projectPath, description});
239
+ expect(execaPipe).toHaveBeenCalledWith(process.stdout);
240
+ expect(resultsReporter.reportResults).toHaveBeenCalledWith(deepmerge.all([languageResults, vcsResults]));
241
+ });
242
+
243
+ it('should consider the language details to be optional', async () => {
244
+ when(optionsValidator.validate)
245
+ .calledWith(options)
246
+ .thenReturn({vcsHosts, decisions, plugins: {languages}});
247
+ scaffoldVcs.mockResolvedValue(vcsResults);
248
+ prompts.promptForBaseDetails.mockResolvedValue({
249
+ [coreQuestionNames.PROJECT_NAME]: projectName,
250
+ [coreQuestionNames.VISIBILITY]: visibility,
251
+ [questionNames.GIT_REPO]: true,
252
+ [coreQuestionNames.LICENSE]: license,
253
+ [coreQuestionNames.DESCRIPTION]: description
254
+ });
255
+ scaffoldLanguage.mockResolvedValue({});
256
+ dependencyUpdaterScaffolder.default.mockResolvedValue({});
257
+ licenseScaffolder.default.mockResolvedValue({});
258
+ scaffoldContributing.mockResolvedValue({});
259
+
260
+ await scaffold(options);
261
+
262
+ expect(scaffoldReadme).toHaveBeenCalledWith({projectName, projectRoot: projectPath, description});
263
+ expect(execa).not.toHaveBeenCalled();
264
+ });
265
+
266
+ it('should pass the license to the language scaffolder as `UNLICENSED` when no license was chosen', async () => {
267
+ when(optionsValidator.validate).calledWith(options).thenReturn({plugins: {languages}, decisions});
268
+ prompts.promptForBaseDetails.mockResolvedValue({});
269
+ scaffoldVcs.mockResolvedValue(vcsResults);
270
+
271
+ await scaffold(options);
272
+
273
+ expect(scaffoldLanguage).toHaveBeenCalledWith(
274
+ languages,
275
+ decisions,
276
+ {
277
+ license: 'UNLICENSED',
278
+ description: undefined,
279
+ projectName: undefined,
280
+ projectRoot: projectPath,
281
+ vcs,
282
+ visibility: undefined
283
+ }
284
+ );
285
+ });
286
+
287
+ it('should not run a verification command when one is not provided', async () => {
288
+ when(optionsValidator.validate).calledWith(options).thenReturn({plugins: {}});
289
+ prompts.promptForBaseDetails.mockResolvedValue({});
290
+ scaffoldVcs.mockResolvedValue({});
291
+ scaffoldLanguage.mockResolvedValue({badges: {}, projectDetails: {}});
292
+
293
+ await scaffold(options);
294
+
295
+ expect(execa).not.toHaveBeenCalled();
296
+ });
297
+ });
@@ -0,0 +1,8 @@
1
+ import {resolve} from 'path';
2
+ import filedirname from 'filedirname';
3
+
4
+ export default function (fileName) {
5
+ const [, __dirname] = filedirname();
6
+
7
+ return resolve(__dirname, '..', 'templates', fileName);
8
+ }
@@ -0,0 +1,14 @@
1
+ import {resolve} from 'node:path';
2
+
3
+ import {describe, expect, it} from 'vitest';
4
+ import any from '@travi/any';
5
+
6
+ import determinePathToTemplateFile from './template-path.js';
7
+
8
+ describe('path to templates', () => {
9
+ it('should provide the proper path to a template file', () => {
10
+ const fileName = any.string();
11
+
12
+ expect(determinePathToTemplateFile(fileName)).toEqual(resolve(__dirname, '..', 'templates', fileName));
13
+ });
14
+ });
@@ -0,0 +1 @@
1
+ export * from './remotes.js';
@@ -0,0 +1,51 @@
1
+ import {simpleGit} from 'simple-git';
2
+ import hostedGitInfo from 'hosted-git-info';
3
+ import {warn} from '@travi/cli-messages';
4
+
5
+ async function getExistingRemotes(git) {
6
+ try {
7
+ return await git.listRemote();
8
+ } catch (e) {
9
+ if ('fatal: No remote configured to list refs from.\n' === e.message) {
10
+ return [];
11
+ }
12
+
13
+ throw e;
14
+ }
15
+ }
16
+
17
+ export async function determineExistingVcsDetails({projectRoot}) {
18
+ const git = simpleGit({baseDir: projectRoot});
19
+ const remoteOrigin = await git.remote(['get-url', 'origin']);
20
+ const {user, project, type} = hostedGitInfo.fromUrl(remoteOrigin);
21
+
22
+ return {vcs: {owner: user, name: project, host: type}};
23
+ }
24
+
25
+ export async function defineRemoteOrigin(projectRoot, sshUrl) {
26
+ if (!sshUrl) {
27
+ warn('URL not available to configure remote `origin`');
28
+
29
+ return {nextSteps: []};
30
+ }
31
+
32
+ const git = simpleGit({baseDir: projectRoot});
33
+ const existingRemotes = await getExistingRemotes(git);
34
+
35
+ if (existingRemotes.includes('origin')) {
36
+ warn('The `origin` remote is already defined for this repository');
37
+
38
+ return {nextSteps: []};
39
+ }
40
+
41
+ // info('Setting the local `master` branch to track `origin/master`');
42
+ //
43
+ // await gitBranch.setUpstream(
44
+ // await gitBranch.lookup(repository, 'master', gitBranch.BRANCH.LOCAL),
45
+ // 'origin/master'
46
+ // );
47
+
48
+ await git.addRemote('origin', sshUrl);
49
+
50
+ return {nextSteps: [{summary: 'Set local `master` branch to track upstream `origin/master`'}]};
51
+ }
@@ -0,0 +1,86 @@
1
+ import {simpleGit} from 'simple-git';
2
+ import hostedGitInfo from 'hosted-git-info';
3
+
4
+ import {describe, vi, it, expect} from 'vitest';
5
+ import {when} from 'vitest-when';
6
+ import any from '@travi/any';
7
+
8
+ import {determineExistingVcsDetails, defineRemoteOrigin} from './remotes.js';
9
+
10
+ vi.mock('simple-git');
11
+ vi.mock('hosted-git-info');
12
+
13
+ describe('Git remote', () => {
14
+ const projectRoot = any.string();
15
+
16
+ describe('determine', () => {
17
+ it('should determine existing vcs details from the remote origin definition of the local repository', async () => {
18
+ const remote = vi.fn();
19
+ const remoteOrigin = any.url();
20
+ const repoName = any.word();
21
+ const vcsHost = `F${any.word()})O${any.word()}O`;
22
+ const vcsHostAccount = any.word();
23
+ when(simpleGit).calledWith({baseDir: projectRoot}).thenReturn({remote});
24
+ when(remote).calledWith(['get-url', 'origin']).thenResolve(remoteOrigin);
25
+ when(hostedGitInfo.fromUrl)
26
+ .calledWith(remoteOrigin)
27
+ .thenReturn({user: vcsHostAccount, project: repoName, type: vcsHost.toLowerCase()});
28
+
29
+ const {vcs: hostDetails} = await determineExistingVcsDetails({projectRoot});
30
+
31
+ expect(hostDetails).toEqual({host: vcsHost.toLowerCase(), owner: vcsHostAccount, name: repoName});
32
+ });
33
+ });
34
+
35
+ describe('define', () => {
36
+ const sshUrl = any.url();
37
+
38
+ it('should define the remote origin', async () => {
39
+ const listRemote = vi.fn();
40
+ const addRemote = vi.fn();
41
+ when(simpleGit).calledWith({baseDir: projectRoot}).thenReturn({listRemote, addRemote});
42
+ when(listRemote).calledWith().thenResolve(any.listOf(any.word));
43
+
44
+ const {nextSteps} = await defineRemoteOrigin(projectRoot, sshUrl);
45
+
46
+ expect(addRemote).toHaveBeenCalledWith('origin', sshUrl);
47
+ expect(nextSteps).toEqual([{summary: 'Set local `master` branch to track upstream `origin/master`'}]);
48
+ });
49
+
50
+ it('should define the remote origin when no remotes are defined', async () => {
51
+ const listRemote = vi.fn();
52
+ const addRemote = vi.fn();
53
+ when(simpleGit).calledWith({baseDir: projectRoot}).thenReturn({listRemote, addRemote});
54
+ when(listRemote).calledWith().thenThrow(new Error('fatal: No remote configured to list refs from.\n'));
55
+
56
+ const {nextSteps} = await defineRemoteOrigin(projectRoot, sshUrl);
57
+
58
+ expect(nextSteps).toEqual([{summary: 'Set local `master` branch to track upstream `origin/master`'}]);
59
+ });
60
+
61
+ it('should throw git errors that are not a lack of defined remotes', async () => {
62
+ const error = new Error(any.sentence());
63
+ const listRemote = vi.fn();
64
+ when(simpleGit).calledWith({baseDir: projectRoot}).thenReturn({listRemote});
65
+ when(listRemote).calledWith().thenThrow(error);
66
+
67
+ await expect(defineRemoteOrigin(projectRoot, sshUrl)).rejects.toThrow(error);
68
+ });
69
+
70
+ it('should return no next-steps when the remote origin is already defined', async () => {
71
+ const listRemote = vi.fn();
72
+ when(simpleGit).calledWith({baseDir: projectRoot}).thenReturn({listRemote});
73
+ when(listRemote).calledWith().thenResolve([...any.listOf(any.word), 'origin', ...any.listOf(any.word)]);
74
+
75
+ const {nextSteps} = await defineRemoteOrigin(projectRoot, sshUrl);
76
+
77
+ expect(nextSteps).toEqual([]);
78
+ });
79
+
80
+ it('should return no next-steps when no `sshUrl` is provided', async () => {
81
+ const {nextSteps} = await defineRemoteOrigin(projectRoot);
82
+
83
+ expect(nextSteps).toEqual([]);
84
+ });
85
+ });
86
+ });
@@ -0,0 +1 @@
1
+ export {default as scaffold} from './scaffolder.js';
@@ -0,0 +1,15 @@
1
+ import {prompt} from '@form8ion/overridable-prompts';
2
+
3
+ import {questionNames} from '../../prompts/question-names.js';
4
+
5
+ export default async function (hosts, decisions) {
6
+ const answers = await prompt([{
7
+ name: questionNames.REPO_HOST,
8
+ type: 'list',
9
+ message: 'Where will the repository be hosted?',
10
+ choices: Object.keys(hosts)
11
+ }], decisions);
12
+ const host = hosts[answers[questionNames.REPO_HOST]];
13
+
14
+ return {...answers, ...host};
15
+ }
@@ -0,0 +1,47 @@
1
+ import * as prompts from '@form8ion/overridable-prompts';
2
+
3
+ import {afterEach, describe, expect, it, vi} from 'vitest';
4
+ import any from '@travi/any';
5
+ import {when} from 'vitest-when';
6
+
7
+ import {questionNames} from '../../prompts/question-names.js';
8
+ import promptForVcsHostDetails from './prompt.js';
9
+
10
+ vi.mock('@form8ion/overridable-prompts');
11
+ vi.mock('../../prompts/conditionals');
12
+
13
+ describe('vcs host details prompt', () => {
14
+ const answers = any.simpleObject();
15
+ const decisions = any.simpleObject();
16
+
17
+ afterEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ it('should prompt for the vcs hosting details', async () => {
22
+ const host = any.string();
23
+ const hostNames = [...any.listOf(any.string), host];
24
+ const hosts = any.objectWithKeys(hostNames, {factory: () => ({})});
25
+ const answersWithHostChoice = {...answers, [questionNames.REPO_HOST]: host};
26
+ when(prompts.prompt).calledWith([{
27
+ name: questionNames.REPO_HOST,
28
+ type: 'list',
29
+ message: 'Where will the repository be hosted?',
30
+ choices: hostNames
31
+ }], decisions).thenResolve(answersWithHostChoice);
32
+
33
+ expect(await promptForVcsHostDetails(hosts, decisions)).toEqual(answersWithHostChoice);
34
+ });
35
+
36
+ it('should not throw an error when `Other` is chosen as the host', async () => {
37
+ const answersWithHostChoice = {...answers, [questionNames.REPO_HOST]: 'Other'};
38
+ when(prompts.prompt).calledWith([{
39
+ name: questionNames.REPO_HOST,
40
+ type: 'list',
41
+ message: 'Where will the repository be hosted?',
42
+ choices: []
43
+ }], decisions).thenResolve(answersWithHostChoice);
44
+
45
+ expect(await promptForVcsHostDetails({}, decisions)).toEqual(answersWithHostChoice);
46
+ });
47
+ });
@@ -0,0 +1,16 @@
1
+ import {questionNames} from '../../prompts/question-names.js';
2
+ import terminalPromptFactory from '../../prompts/terminal-prompt.js';
3
+ import promptForVcsHostDetails from './prompt.js';
4
+
5
+ export default async function scaffoldVcsHost(hosts, decisions, options) {
6
+ const {[questionNames.REPO_HOST]: chosenHost} = await promptForVcsHostDetails(hosts, decisions);
7
+
8
+ const lowercasedHosts = Object.fromEntries(
9
+ Object.entries(hosts).map(([name, details]) => [name.toLowerCase(), details])
10
+ );
11
+ const host = lowercasedHosts[chosenHost.toLowerCase()];
12
+
13
+ if (host) return host.scaffold(options, {prompt: terminalPromptFactory(decisions)});
14
+
15
+ return {vcs: {}};
16
+ }
@@ -0,0 +1,41 @@
1
+ import {describe, expect, it, vi} from 'vitest';
2
+ import any from '@travi/any';
3
+ import {when} from 'vitest-when';
4
+
5
+ import {questionNames} from '../../prompts/question-names.js';
6
+ import terminalPromptFactory from '../../prompts/terminal-prompt.js';
7
+ import promptForVcsHostDetails from './prompt.js';
8
+ import scaffoldVcsHost from './scaffolder.js';
9
+
10
+ vi.mock('../../prompts/terminal-prompt.js');
11
+ vi.mock('./prompt');
12
+
13
+ describe('vcs host scaffolder', () => {
14
+ const options = any.simpleObject();
15
+ const decisions = any.simpleObject();
16
+
17
+ it('should scaffold the chosen vcs host', async () => {
18
+ const chosenHost = `${any.word()}CAPITAL${any.word()}`;
19
+ const results = any.simpleObject();
20
+ const chosenHostScaffolder = vi.fn();
21
+ const hostPlugins = {...any.simpleObject(), [chosenHost.toLowerCase()]: {scaffold: chosenHostScaffolder}};
22
+ const owner = any.word;
23
+ const terminalPrompt = () => undefined;
24
+ when(terminalPromptFactory).calledWith(decisions).thenReturn(terminalPrompt);
25
+ when(promptForVcsHostDetails)
26
+ .calledWith(hostPlugins, decisions)
27
+ .thenResolve({[questionNames.REPO_HOST]: chosenHost, [questionNames.REPO_OWNER]: owner});
28
+ when(chosenHostScaffolder).calledWith(options, {prompt: terminalPrompt}).thenResolve(results);
29
+
30
+ expect(await scaffoldVcsHost(hostPlugins, decisions, options)).toEqual(results);
31
+ });
32
+
33
+ it('should return empty `vcs` results when no matching host is available', async () => {
34
+ const hostPlugins = any.simpleObject();
35
+ when(promptForVcsHostDetails)
36
+ .calledWith(hostPlugins, decisions)
37
+ .thenResolve({[questionNames.REPO_HOST]: any.word()});
38
+
39
+ expect(await scaffoldVcsHost(hostPlugins, decisions, options)).toEqual({vcs: {}});
40
+ });
41
+ });
@@ -0,0 +1,4 @@
1
+ import joi from 'joi';
2
+ import {optionsSchemas} from '@form8ion/core';
3
+
4
+ export default joi.object().pattern(/^/, optionsSchemas.form8ionPlugin);