@servicetitan/startup 31.6.0 → 32.0.1

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 (68) hide show
  1. package/dist/cli/commands/init.d.ts +2 -1
  2. package/dist/cli/commands/init.d.ts.map +1 -1
  3. package/dist/cli/commands/init.js +24 -43
  4. package/dist/cli/commands/init.js.map +1 -1
  5. package/dist/cli/commands/install.d.ts +4 -0
  6. package/dist/cli/commands/install.d.ts.map +1 -1
  7. package/dist/cli/commands/install.js +91 -3
  8. package/dist/cli/commands/install.js.map +1 -1
  9. package/dist/cli/commands/mfe-package-clean.d.ts.map +1 -1
  10. package/dist/cli/commands/mfe-package-clean.js +5 -7
  11. package/dist/cli/commands/mfe-package-clean.js.map +1 -1
  12. package/dist/cli/commands/mfe-package-publish.d.ts.map +1 -1
  13. package/dist/cli/commands/mfe-package-publish.js +11 -15
  14. package/dist/cli/commands/mfe-package-publish.js.map +1 -1
  15. package/dist/cli/commands/upload-sourcemaps.d.ts.map +1 -1
  16. package/dist/cli/commands/upload-sourcemaps.js +1 -1
  17. package/dist/cli/commands/upload-sourcemaps.js.map +1 -1
  18. package/dist/cli/index.js +1 -2
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/utils/cli-git.d.ts +11 -2
  21. package/dist/cli/utils/cli-git.d.ts.map +1 -1
  22. package/dist/cli/utils/cli-git.js +60 -4
  23. package/dist/cli/utils/cli-git.js.map +1 -1
  24. package/dist/cli/utils/cli-os.d.ts.map +1 -1
  25. package/dist/cli/utils/cli-os.js +3 -0
  26. package/dist/cli/utils/cli-os.js.map +1 -1
  27. package/dist/cli/utils/index.d.ts +6 -0
  28. package/dist/cli/utils/index.d.ts.map +1 -1
  29. package/dist/cli/utils/index.js +6 -0
  30. package/dist/cli/utils/index.js.map +1 -1
  31. package/dist/cli/utils/is-ci.d.ts +2 -0
  32. package/dist/cli/utils/is-ci.d.ts.map +1 -0
  33. package/dist/cli/utils/is-ci.js +15 -0
  34. package/dist/cli/utils/is-ci.js.map +1 -0
  35. package/dist/cli/utils/lerna-exec.d.ts.map +1 -1
  36. package/dist/cli/utils/lerna-exec.js +2 -1
  37. package/dist/cli/utils/lerna-exec.js.map +1 -1
  38. package/dist/utils/get-branch-configs.d.ts +1 -1
  39. package/dist/utils/get-branch-configs.d.ts.map +1 -1
  40. package/dist/utils/get-branch-configs.js +2 -2
  41. package/dist/utils/get-branch-configs.js.map +1 -1
  42. package/dist/utils/index.d.ts +1 -0
  43. package/dist/utils/index.d.ts.map +1 -1
  44. package/dist/utils/index.js +1 -0
  45. package/dist/utils/index.js.map +1 -1
  46. package/package.json +6 -6
  47. package/src/cli/commands/__tests__/init.test.ts +21 -87
  48. package/src/cli/commands/__tests__/install.test.ts +174 -12
  49. package/src/cli/commands/__tests__/mfe-package-clean.test.ts +3 -6
  50. package/src/cli/commands/__tests__/mfe-package-publish.test.ts +6 -8
  51. package/src/cli/commands/__tests__/upload-sourcemaps.test.ts +7 -3
  52. package/src/cli/commands/init.ts +17 -37
  53. package/src/cli/commands/install.ts +95 -6
  54. package/src/cli/commands/mfe-package-clean.ts +2 -4
  55. package/src/cli/commands/mfe-package-publish.ts +18 -6
  56. package/src/cli/commands/upload-sourcemaps.ts +2 -2
  57. package/src/cli/index.ts +1 -2
  58. package/src/cli/utils/__tests__/cli-git.test.ts +142 -6
  59. package/src/cli/utils/__tests__/eslint.test.ts +3 -2
  60. package/src/cli/utils/__tests__/is-ci.test.ts +40 -0
  61. package/src/cli/utils/__tests__/lerna-exec.test.ts +6 -3
  62. package/src/cli/utils/cli-git.ts +55 -5
  63. package/src/cli/utils/cli-os.ts +4 -1
  64. package/src/cli/utils/index.ts +6 -0
  65. package/src/cli/utils/is-ci.ts +3 -0
  66. package/src/cli/utils/lerna-exec.ts +2 -1
  67. package/src/utils/get-branch-configs.ts +1 -1
  68. package/src/utils/index.ts +1 -0
@@ -2,40 +2,38 @@ import { fs, vol } from 'memfs';
2
2
  import { mkdirSync, rmSync } from 'fs';
3
3
  import path from 'path';
4
4
 
5
+ import { log } from '../../../utils';
6
+ import { gitCloneRepo, gitIsReachable } from '../../utils';
5
7
  import { Init } from '../init';
6
- import { runCommand, runCommandOutput } from '../../utils/cli-os';
7
8
 
8
9
  jest.mock('fs', () => fs);
9
- jest.mock('../../utils//cli-os', () => ({ runCommand: jest.fn(), runCommandOutput: jest.fn() }));
10
+ jest.mock('../../utils', () => ({
11
+ gitCloneRepo: jest.fn(),
12
+ gitIsReachable: jest.fn(),
13
+ }));
10
14
  jest.mock('../../../utils', () => ({
11
- log: { info: jest.fn() }, // suppress log output
15
+ log: { debug: jest.fn(), error: jest.fn(), info: jest.fn() },
12
16
  }));
13
17
 
14
- const webUrl = 'https://github.com/servicetitan/frontend-example.git';
15
- const sshUrl = 'git@github.com:servicetitan/frontend-example.git';
16
-
17
18
  describe(`[startup] ${Init.name}`, () => {
18
19
  let args: ConstructorParameters<typeof Init>[0];
19
20
 
20
21
  beforeEach(() => {
21
22
  args = {};
22
23
  vol.reset();
23
- jest.clearAllMocks();
24
- jest.mocked(runCommand).mockImplementation(jest.fn());
25
- jest.mocked(runCommandOutput).mockImplementation(jest.fn());
24
+ jest.mocked(gitCloneRepo).mockResolvedValue(true);
26
25
  jest.spyOn(fs, 'mkdirSync').mockImplementation(jest.fn());
27
26
  jest.spyOn(fs, 'rmSync').mockImplementation(jest.fn());
27
+ jest.spyOn(log, 'error').mockImplementation(jest.fn()); // suppress error output
28
28
  });
29
29
 
30
30
  const subject = async () => new Init(args).execute();
31
31
 
32
- test(`clones ${webUrl} to current directory`, async () => {
32
+ test('clones "frontend-example" to current directory', async () => {
33
33
  const cwd = path.resolve('.');
34
34
  await subject();
35
35
 
36
- expect(runCommand).toHaveBeenCalledWith(`git clone -q ${webUrl} ${cwd}`, {
37
- quiet: true,
38
- });
36
+ expect(gitCloneRepo).toHaveBeenCalledWith({ destination: cwd, name: 'frontend-example' });
39
37
  expect(rmSync).toHaveBeenCalledWith(path.join(cwd, '.git'), {
40
38
  recursive: true,
41
39
  force: true,
@@ -44,59 +42,18 @@ describe(`[startup] ${Init.name}`, () => {
44
42
  expect(rmSync).toHaveBeenCalledWith(path.join(cwd, 'package-lock.json'));
45
43
  });
46
44
 
47
- describe(`when cloning ${webUrl} fails`, () => {
48
- beforeEach(() => jest.mocked(runCommand).mockRejectedValueOnce('Nope!'));
49
-
50
- test(`clones ${sshUrl} to current directory`, async () => {
51
- await subject();
52
-
53
- expect(runCommand).toHaveBeenCalledWith(
54
- `git clone -q ${sshUrl} ${path.resolve('.')}`,
55
- expect.anything()
56
- );
57
- });
45
+ describe('when cloning fails', () => {
46
+ beforeEach(() => jest.mocked(gitCloneRepo).mockResolvedValue(false));
58
47
 
59
- describe(`when cloning ${sshUrl} also fails`, () => {
48
+ describe('when repo is not reachable', () => {
60
49
  beforeEach(() => {
61
- jest.mocked(runCommand).mockRejectedValue('Nope!');
62
- });
63
-
64
- test(`checks if ${webUrl} is reachable`, async () => {
65
- await subject();
66
-
67
- expect(runCommandOutput).toHaveBeenCalledWith(`git ls-remote -qt ${webUrl}`, {
68
- quiet: true,
69
- });
50
+ jest.mocked(gitIsReachable).mockReturnValue(false);
70
51
  });
71
52
 
72
- describe(`when ${webUrl} is not reachable`, () => {
73
- beforeEach(() => {
74
- jest.mocked(runCommandOutput).mockImplementationOnce(() => {
75
- throw new Error('Oops!');
76
- });
77
- });
78
-
79
- test(`checks if ${sshUrl} is reachable`, async () => {
80
- await subject();
81
-
82
- expect(runCommandOutput).toHaveBeenCalledWith(`git ls-remote -qt ${sshUrl}`, {
83
- quiet: true,
84
- });
85
- });
86
-
87
- describe(`when ${sshUrl} is also unreachable`, () => {
88
- beforeEach(() =>
89
- jest.mocked(runCommandOutput).mockImplementation(() => {
90
- throw new Error('Oops');
91
- })
92
- );
93
-
94
- test('raises error', async () => {
95
- await expect(subject()).rejects.toThrow(
96
- /could not read servicetitan\/frontend-example repository/
97
- );
98
- });
99
- });
53
+ test('raises error', async () => {
54
+ await expect(subject()).rejects.toThrow(
55
+ /could not read servicetitan\/frontend-example repository/
56
+ );
100
57
  });
101
58
  });
102
59
  });
@@ -104,15 +61,13 @@ describe(`[startup] ${Init.name}`, () => {
104
61
  describe('with an output location', () => {
105
62
  beforeEach(() => (args.output = 'foo/bar'));
106
63
 
107
- test(`clones ${webUrl} to output location`, async () => {
64
+ test('clones repo to output location', async () => {
108
65
  const destination = path.resolve(args.output!);
109
66
 
110
67
  await subject();
111
68
 
112
69
  expect(mkdirSync).toHaveBeenCalledWith(destination, { recursive: true });
113
- expect(runCommand).toHaveBeenCalledWith(`git clone -q ${webUrl} ${destination}`, {
114
- quiet: true,
115
- });
70
+ expect(gitCloneRepo).toHaveBeenCalledWith(expect.objectContaining({ destination }));
116
71
  });
117
72
 
118
73
  describe('when output location is a file', () => {
@@ -131,25 +86,4 @@ describe(`[startup] ${Init.name}`, () => {
131
86
  });
132
87
  });
133
88
  });
134
-
135
- describe('when running in CI environment with GITHUB_TOKEN', () => {
136
- const originalEnv = process.env;
137
- const token = 'foo-bar';
138
-
139
- beforeEach(() => {
140
- process.env.CI = 'true';
141
- process.env.GITHUB_TOKEN = token;
142
- });
143
- afterEach(() => (process.env = originalEnv));
144
-
145
- test(`adds token to ${webUrl}`, async () => {
146
- await subject();
147
-
148
- const urlWithToken = webUrl.replace('github.com', `oauth2:${token}@github.com`);
149
- expect(runCommand).toHaveBeenCalledWith(
150
- `git clone -q ${urlWithToken} ${path.resolve('.')}`,
151
- expect.anything()
152
- );
153
- });
154
- });
155
89
  });
@@ -1,33 +1,92 @@
1
+ import { vol, fs } from 'memfs';
1
2
  import { execSync } from 'child_process';
2
- import { log } from '../../../utils';
3
+ import { getStartupVersion, log } from '../../../utils';
4
+ import { gitCloneRepo, isCI } from '../../utils';
3
5
 
4
6
  import { Install } from '../install';
5
7
 
8
+ jest.mock('fs', () => fs);
6
9
  jest.mock('child_process', () => ({ execSync: jest.fn() }));
10
+ jest.mock('../../utils', () => ({
11
+ ...jest.requireActual('../../utils'),
12
+ gitCloneRepo: jest.fn(),
13
+ isCI: jest.fn(),
14
+ }));
7
15
  jest.mock('../../../utils', () => ({
8
16
  ...jest.requireActual('../../../utils'),
9
- log: { info: jest.fn() }, // suppress log output
17
+ getStartupVersion: jest.fn(),
18
+ log: { debug: jest.fn(), info: jest.fn(), warning: jest.fn() }, // suppress log output
10
19
  }));
11
20
 
12
21
  describe(`${Install.name}`, () => {
13
- const OLD_ENV = process.env;
14
- const options = ['--audit=false', '--fund=false', '--legacy-peer-deps'];
22
+ const mockNpmToken = 'npm_Foo';
23
+ const npmOptions = ['--audit=false', '--fund=false', '--legacy-peer-deps'];
24
+ const startupVersion = '1.2.3';
25
+ const tempDirPath = 'tempDirPath';
15
26
  let args: ConstructorParameters<typeof Install>[0];
16
27
 
28
+ function volFromJSON(overrides?: Record<string, string>) {
29
+ vol.fromJSON({
30
+ '.npmrc': '',
31
+ // Mock cloned Github repo with .npm.json containing readOnlyToken
32
+ [`${tempDirPath}/.npm.json`]: JSON.stringify({
33
+ readOnlyToken: Buffer.from(mockNpmToken).toString('base64'),
34
+ }),
35
+ ...overrides,
36
+ });
37
+ }
38
+
17
39
  beforeEach(() => {
18
- process.env = {};
19
40
  args = undefined;
20
41
  jest.clearAllMocks();
42
+ jest.mocked(gitCloneRepo).mockResolvedValue(true);
43
+ jest.mocked(getStartupVersion).mockReturnValue(startupVersion);
44
+ jest.mocked(isCI).mockReturnValue(false);
45
+ jest.spyOn(fs, 'mkdtempSync').mockImplementation(() => tempDirPath);
46
+ volFromJSON();
21
47
  });
22
48
 
23
- afterEach(() => (process.env = OLD_ENV));
49
+ afterEach(() => vol.reset());
24
50
 
25
51
  const subject = async () => new Install(args).execute();
26
52
 
27
- test(`runs npm i ${options.join(' ')}`, async () => {
53
+ test('clones "frontend-dev-config" repo to temp directory', async () => {
54
+ await subject();
55
+
56
+ expect(gitCloneRepo).toHaveBeenCalledWith({
57
+ destination: tempDirPath,
58
+ name: 'frontend-dev-config',
59
+ });
60
+ });
61
+
62
+ test('configures NPM token with value from cloned repo', async () => {
63
+ await subject();
64
+
65
+ expect(execSync).toHaveBeenCalledWith(
66
+ `npm config set "//registry.npmjs.org/:_authToken"="${mockNpmToken}"`
67
+ );
68
+ });
69
+
70
+ test('deletes NPM token from project .npmrc', async () => {
71
+ await subject();
72
+
73
+ expect(execSync).toHaveBeenCalledWith(
74
+ `npm config delete --location=project "//registry.npmjs.org/:_authToken"`
75
+ );
76
+ });
77
+
78
+ test('removes temp directory', async () => {
79
+ const rmSpy = jest.spyOn(fs, 'rmSync');
80
+
28
81
  await subject();
29
82
 
30
- expect(execSync).toHaveBeenCalledWith(`npm i ${options.join(' ')}`, {
83
+ expect(rmSpy).toHaveBeenCalledWith(tempDirPath, { recursive: true, force: true });
84
+ });
85
+
86
+ test(`runs npm i ${npmOptions.join(' ')}`, async () => {
87
+ await subject();
88
+
89
+ expect(execSync).toHaveBeenCalledWith(`npm i ${npmOptions.join(' ')}`, {
31
90
  stdio: 'inherit',
32
91
  });
33
92
  });
@@ -35,19 +94,72 @@ describe(`${Install.name}`, () => {
35
94
  test('logs progress', async () => {
36
95
  await subject();
37
96
 
38
- expect(log.info).toHaveBeenCalled();
97
+ [`startup cli v${startupVersion}`, 'Configuring NPM token', 'Running npm'].forEach(
98
+ message => {
99
+ expect(log.info).toHaveBeenCalledWith(expect.stringMatching(message));
100
+ }
101
+ );
102
+ });
103
+
104
+ function itDoesNotConfigureNpmToken() {
105
+ test('does not configure NPM token', async () => {
106
+ await subject();
107
+
108
+ expect(execSync).not.toHaveBeenCalledWith(expect.stringMatching(/npm config/));
109
+ });
110
+ }
111
+
112
+ function itDoesNotDeleteProjectToken() {
113
+ test('does not delete project token', async () => {
114
+ await subject();
115
+
116
+ expect(execSync).not.toHaveBeenCalledWith(expect.stringMatching(/npm config delete/));
117
+ });
118
+ }
119
+
120
+ describe('when .npmrc uses an environment variable', () => {
121
+ beforeEach(() => {
122
+ volFromJSON({
123
+ '.npmrc': [
124
+ // eslint-disable-next-line no-template-curly-in-string
125
+ '#//registry.npmjs.org/:_authToken=${NPM_READONLY_TOKEN}', // should ignore this comment
126
+ // eslint-disable-next-line no-template-curly-in-string
127
+ '//registry.npmjs.org/:_authToken=${ST_NPM_READONLY_TOKEN}',
128
+ ].join('\n'),
129
+ });
130
+ });
131
+
132
+ test('runs npm i with environment variable', async () => {
133
+ await subject();
134
+
135
+ expect(execSync).toHaveBeenCalledWith(`npm i ${npmOptions.join(' ')}`, {
136
+ // eslint-disable-next-line @typescript-eslint/naming-convention
137
+ env: expect.objectContaining({ ST_NPM_READONLY_TOKEN: mockNpmToken }),
138
+ stdio: 'inherit',
139
+ });
140
+ });
141
+
142
+ itDoesNotDeleteProjectToken();
143
+ });
144
+
145
+ describe('with no .npmrc', () => {
146
+ beforeEach(() => fs.rmSync('.npmrc'));
147
+
148
+ itDoesNotDeleteProjectToken();
39
149
  });
40
150
 
41
151
  describe('when in CI environment', () => {
42
- beforeEach(() => (process.env.CI = 'true'));
152
+ beforeEach(() => jest.mocked(isCI).mockReturnValue(true));
43
153
 
44
154
  test('runs npm ci', async () => {
45
155
  await subject();
46
156
 
47
- expect(execSync).toHaveBeenCalledWith(`npm ci ${options.join(' ')}`, {
157
+ expect(execSync).toHaveBeenCalledWith(`npm ci ${npmOptions.join(' ')}`, {
48
158
  stdio: 'inherit',
49
159
  });
50
160
  });
161
+
162
+ itDoesNotConfigureNpmToken();
51
163
  });
52
164
 
53
165
  describe('with --quite', () => {
@@ -61,7 +173,7 @@ describe(`${Install.name}`, () => {
61
173
  });
62
174
 
63
175
  describe('with --fix', () => {
64
- const fixOptions = [...options, '--package-lock-only', '--prefer-dedupe'];
176
+ const fixOptions = [...npmOptions, '--package-lock-only', '--prefer-dedupe'];
65
177
 
66
178
  beforeEach(() => (args = { fix: true }));
67
179
 
@@ -73,5 +185,55 @@ describe(`${Install.name}`, () => {
73
185
  expect.anything()
74
186
  );
75
187
  });
188
+
189
+ itDoesNotConfigureNpmToken();
190
+ });
191
+
192
+ describe('with --no-token', () => {
193
+ beforeEach(() => (args = { token: false }));
194
+
195
+ itDoesNotConfigureNpmToken();
196
+ });
197
+
198
+ function itLogsError(message: string | RegExp) {
199
+ test('logs error', async () => {
200
+ await subject();
201
+
202
+ expect(log.warning).toHaveBeenCalledWith(expect.stringMatching(message));
203
+ });
204
+ }
205
+
206
+ describe('when error occurs fetching token', () => {
207
+ beforeEach(() => jest.mocked(gitCloneRepo).mockResolvedValue(false));
208
+
209
+ itLogsError(/could not clone servicetitan\/frontend-dev-config/);
210
+ });
211
+
212
+ describe('when .npm.json is not an object', () => {
213
+ beforeEach(() => volFromJSON({ [`${tempDirPath}/.npm.json`]: JSON.stringify('') }));
214
+
215
+ itLogsError(/is not an object/);
216
+ });
217
+
218
+ describe('when .npm.json omits readOnlyToken', () => {
219
+ beforeEach(() => volFromJSON({ [`${tempDirPath}/.npm.json`]: JSON.stringify({}) }));
220
+
221
+ itLogsError(/does not contain auth token/);
222
+ });
223
+
224
+ describe('when readOnlyToken is blank', () => {
225
+ beforeEach(() =>
226
+ volFromJSON({ [`${tempDirPath}/.npm.json`]: JSON.stringify({ readOnlyToken: '' }) })
227
+ );
228
+
229
+ itLogsError(/does not contain auth token/);
230
+ });
231
+
232
+ describe('when readOnlyToken is not a string', () => {
233
+ beforeEach(() =>
234
+ volFromJSON({ [`${tempDirPath}/.npm.json`]: JSON.stringify({ readOnlyToken: {} }) })
235
+ );
236
+
237
+ itLogsError(/token is not a string/);
76
238
  });
77
239
  });
@@ -1,7 +1,6 @@
1
1
  import { fs, vol } from 'memfs';
2
2
  import { isWebComponent, log } from '../../../utils';
3
- import { gitGetBranch } from '../../utils/cli-git';
4
- import { Version, npmGetPackageVersionsDetails, npmUnpublish } from '../../utils/cli-npm';
3
+ import { gitGetBranch, npmGetPackageVersionsDetails, npmUnpublish, Version } from '../../utils';
5
4
 
6
5
  import { MFEPackageClean } from '../mfe-package-clean';
7
6
 
@@ -11,13 +10,11 @@ jest.mock('../../../utils', () => ({
11
10
  isWebComponent: jest.fn(),
12
11
  log: { error: jest.fn(), info: jest.fn() },
13
12
  }));
14
- jest.mock('../../utils/cli-npm', () => ({
13
+ jest.mock('../../utils', () => ({
14
+ gitGetBranch: jest.fn(),
15
15
  npmGetPackageVersionsDetails: jest.fn(),
16
16
  npmUnpublish: jest.fn(),
17
17
  }));
18
- jest.mock('../../utils/cli-git', () => ({
19
- gitGetBranch: jest.fn(),
20
- }));
21
18
 
22
19
  const DEFAULT_VERSIONS_TO_KEEP = 5;
23
20
 
@@ -1,14 +1,15 @@
1
1
  import { fs, vol } from 'memfs';
2
2
  import path from 'path';
3
3
  import { isWebComponent, log, readJson, WebComponentBranchConfigs } from '../../../utils';
4
- import { gitGetBranch, gitGetCommitHash } from '../../utils/cli-git';
5
4
  import {
5
+ gitGetBranch,
6
+ gitGetCommitHash,
6
7
  npmGetPackageVersions,
7
8
  npmPackageSet,
8
9
  npmPublish,
9
10
  npmTagVersion,
10
- } from '../../utils/cli-npm';
11
- import { runCommand } from '../../utils/cli-os';
11
+ runCommand,
12
+ } from '../../utils';
12
13
  import { MFEPackagePublish } from '../mfe-package-publish';
13
14
 
14
15
  jest.mock('fs', () => fs);
@@ -17,17 +18,14 @@ jest.mock('../../../utils', () => ({
17
18
  isWebComponent: jest.fn(),
18
19
  log: { info: jest.fn(), warning: jest.fn() },
19
20
  }));
20
- jest.mock('../../utils/cli-git', () => ({
21
+ jest.mock('../../utils', () => ({
22
+ ...jest.requireActual('../../utils'),
21
23
  gitGetBranch: jest.fn(),
22
24
  gitGetCommitHash: jest.fn(),
23
- }));
24
- jest.mock('../../utils/cli-npm', () => ({
25
25
  npmGetPackageVersions: jest.fn(),
26
26
  npmPackageSet: jest.fn(),
27
27
  npmPublish: jest.fn(),
28
28
  npmTagVersion: jest.fn(),
29
- }));
30
- jest.mock('../../utils/cli-os', () => ({
31
29
  runCommand: jest.fn(),
32
30
  }));
33
31
 
@@ -2,11 +2,16 @@ import { execSync } from 'child_process';
2
2
  import { vol, fs } from 'memfs';
3
3
  import { inspect } from 'node:util';
4
4
  import path from 'path';
5
+ import { isCI } from '../../utils';
5
6
  import { log } from '../../../utils';
6
7
  import { UploadSourcemaps } from '../upload-sourcemaps';
7
8
 
8
9
  jest.mock('child_process', () => ({ execSync: jest.fn() }));
9
10
  jest.mock('fs', () => fs);
11
+ jest.mock('../../utils', () => ({
12
+ ...jest.requireActual('../../utils'),
13
+ isCI: jest.fn(),
14
+ }));
10
15
  jest.mock('../../../utils', () => ({
11
16
  ...jest.requireActual('../../../utils'),
12
17
  log: { info: jest.fn(), warning: jest.fn() },
@@ -33,6 +38,7 @@ describe(`[startup] ${UploadSourcemaps.name}`, () => {
33
38
 
34
39
  beforeEach(() => {
35
40
  jest.clearAllMocks();
41
+ jest.mocked(isCI).mockReturnValue(false);
36
42
 
37
43
  // eslint-disable-next-line @typescript-eslint/naming-convention
38
44
  process.env = { DATADOG_API_KEY: 'datadog-api-key' };
@@ -91,9 +97,7 @@ describe(`[startup] ${UploadSourcemaps.name}`, () => {
91
97
  itThrowsError('DATADOG_API_KEY environment variable is not set');
92
98
 
93
99
  describe('when in CI environment', () => {
94
- beforeEach(() => (process.env.CI = '1'));
95
-
96
- afterEach(() => delete process.env.CI);
100
+ beforeEach(() => jest.mocked(isCI).mockReturnValue(true));
97
101
 
98
102
  test('logs warning', async () => {
99
103
  await subject();
@@ -2,15 +2,14 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
 
4
4
  import { log, logErrors } from '../../utils';
5
- import { runCommand, runCommandOutput } from '../utils/cli-os';
6
- import { Command } from './';
5
+ import { gitCloneRepo, gitIsReachable } from '../utils';
6
+ import { Command } from './types';
7
7
 
8
8
  interface Args {
9
9
  output?: string;
10
10
  }
11
11
 
12
- const webUrl = 'https://github.com/servicetitan/frontend-example.git';
13
- const sshUrl = 'git@github.com:servicetitan/frontend-example.git';
12
+ const REPO_NAME = 'frontend-example';
14
13
 
15
14
  export class Init implements Command {
16
15
  constructor(private readonly args: Args) {}
@@ -30,44 +29,25 @@ export class Init implements Command {
30
29
  throw new Error(`${destination} is not an empty directory`);
31
30
  }
32
31
 
33
- const gitUrls = [webUrl, sshUrl];
34
- if (!!process.env.CI && !!process.env.GITHUB_TOKEN) {
35
- gitUrls.unshift(
36
- webUrl.replace('github.com', `oauth2:${process.env.GITHUB_TOKEN}@github.com`)
37
- );
32
+ if (await this.cloneRepo(destination)) {
33
+ log.info(`copied example project to ${destination}`);
34
+ return;
38
35
  }
39
36
 
40
- for (const url of gitUrls) {
41
- // eslint-disable-next-line no-await-in-loop
42
- if (await cloneRepo(url, destination)) {
43
- log.info(`copied example project to ${destination}`);
44
- return;
45
- }
46
- }
47
-
48
- if (!gitUrls.some(isReachable)) {
49
- throw new Error('could not read servicetitan/frontend-example repository');
37
+ if (!gitIsReachable({ name: REPO_NAME })) {
38
+ throw new Error(`could not read servicetitan/${REPO_NAME} repository`);
50
39
  }
51
40
  }
52
- }
53
41
 
54
- async function cloneRepo(url: string, destination: string) {
55
- try {
56
- await runCommand(`git clone -q ${url} ${destination}`, { quiet: true });
57
- } catch {
58
- return false;
59
- }
60
- fs.rmSync(path.join(destination, '.git'), { recursive: true, force: true });
61
- fs.rmSync(path.join(destination, '.github', 'CODEOWNERS'));
62
- fs.rmSync(path.join(destination, 'package-lock.json'));
63
- return true;
64
- }
42
+ async cloneRepo(destination: string) {
43
+ const ok = await gitCloneRepo({ destination, name: REPO_NAME });
44
+ if (!ok) {
45
+ return false;
46
+ }
65
47
 
66
- function isReachable(url: string) {
67
- try {
68
- runCommandOutput(`git ls-remote -qt ${url}`, { quiet: true });
69
- } catch {
70
- return false;
48
+ fs.rmSync(path.join(destination, '.git'), { recursive: true, force: true });
49
+ fs.rmSync(path.join(destination, '.github', 'CODEOWNERS'));
50
+ fs.rmSync(path.join(destination, 'package-lock.json'));
51
+ return true;
71
52
  }
72
- return true;
73
53
  }