@servicetitan/startup 32.4.0 → 32.6.0

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 (65) hide show
  1. package/dist/cli/commands/init.d.ts +1 -1
  2. package/dist/cli/commands/init.d.ts.map +1 -1
  3. package/dist/cli/commands/init.js +6 -5
  4. package/dist/cli/commands/init.js.map +1 -1
  5. package/dist/cli/commands/install.d.ts +1 -4
  6. package/dist/cli/commands/install.d.ts.map +1 -1
  7. package/dist/cli/commands/install.js +12 -118
  8. package/dist/cli/commands/install.js.map +1 -1
  9. package/dist/cli/commands/mfe-list.d.ts.map +1 -1
  10. package/dist/cli/commands/mfe-list.js +2 -1
  11. package/dist/cli/commands/mfe-list.js.map +1 -1
  12. package/dist/cli/commands/review/rules/index.d.ts.map +1 -1
  13. package/dist/cli/commands/review/rules/index.js +2 -0
  14. package/dist/cli/commands/review/rules/index.js.map +1 -1
  15. package/dist/cli/commands/review/rules/no-deprecated-startup-install.d.ts +7 -0
  16. package/dist/cli/commands/review/rules/no-deprecated-startup-install.d.ts.map +1 -0
  17. package/dist/cli/commands/review/rules/no-deprecated-startup-install.js +38 -0
  18. package/dist/cli/commands/review/rules/no-deprecated-startup-install.js.map +1 -0
  19. package/dist/cli/commands/review/types.d.ts +1 -0
  20. package/dist/cli/commands/review/types.d.ts.map +1 -1
  21. package/dist/cli/commands/review/types.js.map +1 -1
  22. package/dist/cli/commands/upload-sourcemaps.d.ts.map +1 -1
  23. package/dist/cli/commands/upload-sourcemaps.js +2 -1
  24. package/dist/cli/commands/upload-sourcemaps.js.map +1 -1
  25. package/dist/cli/utils/cli-git.d.ts +0 -9
  26. package/dist/cli/utils/cli-git.d.ts.map +1 -1
  27. package/dist/cli/utils/cli-git.js +0 -59
  28. package/dist/cli/utils/cli-git.js.map +1 -1
  29. package/dist/cli/utils/cli-npm.d.ts +0 -3
  30. package/dist/cli/utils/cli-npm.d.ts.map +1 -1
  31. package/dist/cli/utils/cli-npm.js +0 -22
  32. package/dist/cli/utils/cli-npm.js.map +1 -1
  33. package/dist/cli/utils/index.d.ts +0 -1
  34. package/dist/cli/utils/index.d.ts.map +1 -1
  35. package/dist/cli/utils/index.js +0 -1
  36. package/dist/cli/utils/index.js.map +1 -1
  37. package/dist/cli/utils/lerna-exec.d.ts.map +1 -1
  38. package/dist/cli/utils/lerna-exec.js +2 -2
  39. package/dist/cli/utils/lerna-exec.js.map +1 -1
  40. package/package.json +11 -8
  41. package/src/cli/commands/__tests__/init.test.ts +11 -14
  42. package/src/cli/commands/__tests__/install.test.ts +19 -224
  43. package/src/cli/commands/__tests__/mfe-list.test.ts +5 -4
  44. package/src/cli/commands/__tests__/upload-sourcemaps.test.ts +4 -7
  45. package/src/cli/commands/init.ts +6 -4
  46. package/src/cli/commands/install.ts +13 -116
  47. package/src/cli/commands/mfe-list.ts +2 -1
  48. package/src/cli/commands/review/rules/__tests__/no-deprecated-startup-install.test.ts +81 -0
  49. package/src/cli/commands/review/rules/index.ts +2 -0
  50. package/src/cli/commands/review/rules/no-deprecated-startup-install.ts +30 -0
  51. package/src/cli/commands/review/types.ts +1 -0
  52. package/src/cli/commands/upload-sourcemaps.ts +2 -1
  53. package/src/cli/utils/__tests__/cli-git.test.ts +4 -140
  54. package/src/cli/utils/__tests__/cli-npm.test.ts +0 -43
  55. package/src/cli/utils/__tests__/lerna-exec.test.ts +2 -2
  56. package/src/cli/utils/cli-git.ts +1 -52
  57. package/src/cli/utils/cli-npm.ts +0 -12
  58. package/src/cli/utils/index.ts +0 -1
  59. package/src/cli/utils/lerna-exec.ts +1 -1
  60. package/dist/cli/utils/is-ci.d.ts +0 -2
  61. package/dist/cli/utils/is-ci.d.ts.map +0 -1
  62. package/dist/cli/utils/is-ci.js +0 -15
  63. package/dist/cli/utils/is-ci.js.map +0 -1
  64. package/src/cli/utils/__tests__/is-ci.test.ts +0 -40
  65. package/src/cli/utils/is-ci.ts +0 -3
@@ -1,277 +1,72 @@
1
- import { vol, fs } from 'memfs';
2
1
  import { execSync } from 'child_process';
3
2
  import { getStartupVersion, log } from '../../../utils';
4
- import { gitCloneRepo, isCI, npmWhoAmI } from '../../utils';
5
3
 
6
4
  import { Install } from '../install';
7
5
 
8
- jest.mock('fs', () => fs);
9
6
  jest.mock('child_process', () => ({ execSync: jest.fn() }));
10
- jest.mock('../../utils', () => ({
11
- ...jest.requireActual('../../utils'),
12
- gitCloneRepo: jest.fn(),
13
- isCI: jest.fn(),
14
- npmWhoAmI: jest.fn(),
15
- }));
16
7
  jest.mock('../../../utils', () => ({
17
8
  ...jest.requireActual('../../../utils'),
18
9
  getStartupVersion: jest.fn(),
19
- log: { debug: jest.fn(), info: jest.fn(), warning: jest.fn() }, // suppress log output
10
+ log: { debug: jest.fn(), info: jest.fn() }, // suppress log output
20
11
  }));
21
12
 
22
13
  describe(`${Install.name}`, () => {
23
- const mockNpmToken = 'npm_Foo';
24
- const npmOptions = ['--audit=false', '--fund=false', '--legacy-peer-deps'];
25
14
  const startupVersion = '1.2.3';
26
- const tempDirPath = 'tempDirPath';
27
15
  let args: ConstructorParameters<typeof Install>[0];
28
16
 
29
- function volFromJSON(overrides?: Record<string, string>) {
30
- vol.fromJSON({
31
- '.npmrc': '',
32
- // Mock cloned Github repo with .npm.json containing readOnlyToken
33
- [`${tempDirPath}/.npm.json`]: JSON.stringify({
34
- readOnlyToken: Buffer.from(mockNpmToken).toString('base64'),
35
- }),
36
- ...overrides,
37
- });
38
- }
39
-
40
17
  beforeEach(() => {
41
18
  args = {};
42
19
  jest.clearAllMocks();
43
- jest.mocked(gitCloneRepo).mockResolvedValue(true);
44
20
  jest.mocked(getStartupVersion).mockReturnValue(startupVersion);
45
- jest.mocked(isCI).mockReturnValue(false);
46
- jest.mocked(npmWhoAmI).mockReturnValue(undefined);
47
- jest.spyOn(fs, 'mkdtempSync').mockImplementation(() => tempDirPath);
48
- volFromJSON();
49
21
  });
50
22
 
51
- afterEach(() => vol.reset());
52
-
53
23
  const subject = async () => new Install(args).execute();
54
24
 
55
- test('clones "frontend-dev-config" repo to temp directory', async () => {
56
- await subject();
57
-
58
- expect(gitCloneRepo).toHaveBeenCalledWith({
59
- destination: tempDirPath,
60
- name: 'frontend-dev-config',
61
- });
62
- });
63
-
64
- test('configures NPM token with value from cloned repo', async () => {
65
- await subject();
66
-
67
- expect(execSync).toHaveBeenCalledWith(
68
- `npm config set "//registry.npmjs.org/:_authToken"="${mockNpmToken}"`
69
- );
70
- });
71
-
72
- test('deletes NPM token from project .npmrc', async () => {
25
+ test('runs install', async () => {
73
26
  await subject();
74
27
 
75
- expect(execSync).toHaveBeenCalledWith(
76
- `npm config delete --location=project "//registry.npmjs.org/:_authToken"`
77
- );
28
+ expect(execSync).toHaveBeenCalledWith(`npx @servicetitan/install`, { stdio: 'inherit' });
78
29
  });
79
30
 
80
- test('removes temp directory', async () => {
81
- const rmSpy = jest.spyOn(fs, 'rmSync');
82
-
83
- await subject();
84
-
85
- expect(rmSpy).toHaveBeenCalledWith(tempDirPath, { recursive: true, force: true });
86
- });
87
-
88
- test(`runs npm i ${npmOptions.join(' ')}`, async () => {
89
- await subject();
90
-
91
- expect(execSync).toHaveBeenCalledWith(`npm i ${npmOptions.join(' ')}`, {
92
- stdio: 'inherit',
93
- });
94
- });
95
-
96
- test('logs progress', async () => {
97
- await subject();
31
+ describe.each(['fix', 'quiet', 'token'])('with --%s', option => {
32
+ beforeEach(() => (args[option] = true));
98
33
 
99
- [`startup cli v${startupVersion}`, 'Configuring NPM token', 'Running npm'].forEach(
100
- message => {
101
- expect(log.info).toHaveBeenCalledWith(expect.stringMatching(message));
102
- }
103
- );
104
- });
105
-
106
- function itDoesNotConfigureNpmToken() {
107
- test('does not configure NPM token', async () => {
108
- await subject();
109
-
110
- expect(gitCloneRepo).not.toHaveBeenCalled();
111
- expect(execSync).not.toHaveBeenCalledWith(
112
- expect.stringMatching(/npm config/),
113
- expect.anything()
114
- );
115
- });
116
- }
117
-
118
- function itDoesNotDeleteProjectToken() {
119
- test('does not delete project token', async () => {
34
+ test(`runs install with --${option}`, async () => {
120
35
  await subject();
121
36
 
122
- expect(execSync).not.toHaveBeenCalledWith(
123
- expect.stringMatching(/npm config delete/),
37
+ expect(execSync).toHaveBeenCalledWith(
38
+ expect.stringContaining(`--${option}`),
124
39
  expect.anything()
125
40
  );
126
41
  });
127
- }
128
-
129
- describe('when "st-team" is already logged in', () => {
130
- beforeEach(() => jest.mocked(npmWhoAmI).mockReturnValue('st-team'));
131
-
132
- itDoesNotConfigureNpmToken();
133
-
134
- describe('with --token', () => {
135
- beforeEach(() => (args = { token: true }));
136
-
137
- test('configures NPM token', async () => {
138
- await subject();
139
-
140
- expect(execSync).toHaveBeenCalledWith(expect.stringMatching(/npm config set/));
141
- });
142
- });
143
- });
144
-
145
- describe('when .npmrc uses an environment variable', () => {
146
- beforeEach(() => {
147
- volFromJSON({
148
- '.npmrc': [
149
- // eslint-disable-next-line no-template-curly-in-string
150
- '#//registry.npmjs.org/:_authToken=${NPM_READONLY_TOKEN}', // should ignore this comment
151
- // eslint-disable-next-line no-template-curly-in-string
152
- '//registry.npmjs.org/:_authToken=${ST_NPM_READONLY_TOKEN}',
153
- ].join('\n'),
154
- });
155
- });
156
-
157
- test('runs npm i with environment variable', async () => {
158
- await subject();
159
-
160
- expect(execSync).toHaveBeenCalledWith(`npm i ${npmOptions.join(' ')}`, {
161
- // eslint-disable-next-line @typescript-eslint/naming-convention
162
- env: expect.objectContaining({ ST_NPM_READONLY_TOKEN: mockNpmToken }),
163
- stdio: 'inherit',
164
- });
165
- });
166
-
167
- itDoesNotDeleteProjectToken();
168
42
  });
169
43
 
170
- describe('with no .npmrc', () => {
171
- beforeEach(() => fs.rmSync('.npmrc'));
172
-
173
- itDoesNotDeleteProjectToken();
174
- });
175
-
176
- describe('when in CI environment', () => {
177
- beforeEach(() => jest.mocked(isCI).mockReturnValue(true));
178
-
179
- test('runs npm ci', async () => {
180
- await subject();
181
-
182
- expect(execSync).toHaveBeenCalledWith(`npm ci ${npmOptions.join(' ')}`, {
183
- stdio: 'inherit',
184
- });
185
- });
186
-
187
- itDoesNotConfigureNpmToken();
188
- });
189
-
190
- describe('with --quite', () => {
191
- beforeEach(() => (args = { quiet: true }));
192
-
193
- test('does not log progress', async () => {
194
- await subject();
195
-
196
- expect(log.info).not.toHaveBeenCalled();
197
- });
198
- });
199
-
200
- describe('with --fix', () => {
201
- const fixOptions = [...npmOptions, '--package-lock-only', '--prefer-dedupe'];
202
-
203
- beforeEach(() => (args = { fix: true }));
44
+ describe('with --no-token', () => {
45
+ beforeEach(() => (args.token = false));
204
46
 
205
- test(`runs npm i ${fixOptions.join(' ')}`, async () => {
47
+ test('runs install with --no-token', async () => {
206
48
  await subject();
207
49
 
208
50
  expect(execSync).toHaveBeenCalledWith(
209
- `npm i ${fixOptions.join(' ')}`,
51
+ expect.stringContaining('--no-token'),
210
52
  expect.anything()
211
53
  );
212
54
  });
213
-
214
- itDoesNotConfigureNpmToken();
215
55
  });
216
56
 
217
- describe('with --no-token', () => {
218
- beforeEach(() => (args = { token: false }));
57
+ test('logs progress', async () => {
58
+ await subject();
219
59
 
220
- itDoesNotConfigureNpmToken();
60
+ expect(log.info).toHaveBeenCalledWith(`startup cli v${startupVersion}`);
221
61
  });
222
62
 
223
- describe('with --token', () => {
224
- beforeEach(() => (args = { token: true }));
225
-
226
- test('does not run "npm i"', async () => {
227
- await subject();
228
-
229
- expect(execSync).not.toHaveBeenCalledWith(
230
- expect.stringMatching(/npm i/),
231
- expect.anything()
232
- );
233
- });
234
- });
63
+ describe('with --quiet', () => {
64
+ beforeEach(() => (args = { quiet: true }));
235
65
 
236
- function itLogsError(message: string | RegExp) {
237
- test('logs error', async () => {
66
+ test('does not log progress', async () => {
238
67
  await subject();
239
68
 
240
- expect(log.warning).toHaveBeenCalledWith(expect.stringMatching(message));
69
+ expect(log.info).not.toHaveBeenCalled();
241
70
  });
242
- }
243
-
244
- describe('when error occurs fetching token', () => {
245
- beforeEach(() => jest.mocked(gitCloneRepo).mockResolvedValue(false));
246
-
247
- itLogsError(/could not clone servicetitan\/frontend-dev-config/);
248
- });
249
-
250
- describe('when .npm.json is not an object', () => {
251
- beforeEach(() => volFromJSON({ [`${tempDirPath}/.npm.json`]: JSON.stringify('') }));
252
-
253
- itLogsError(/is not an object/);
254
- });
255
-
256
- describe('when .npm.json omits readOnlyToken', () => {
257
- beforeEach(() => volFromJSON({ [`${tempDirPath}/.npm.json`]: JSON.stringify({}) }));
258
-
259
- itLogsError(/does not contain auth token/);
260
- });
261
-
262
- describe('when readOnlyToken is blank', () => {
263
- beforeEach(() =>
264
- volFromJSON({ [`${tempDirPath}/.npm.json`]: JSON.stringify({ readOnlyToken: '' }) })
265
- );
266
-
267
- itLogsError(/does not contain auth token/);
268
- });
269
-
270
- describe('when readOnlyToken is not a string', () => {
271
- beforeEach(() =>
272
- volFromJSON({ [`${tempDirPath}/.npm.json`]: JSON.stringify({ readOnlyToken: {} }) })
273
- );
274
-
275
- itLogsError(/token is not a string/);
276
71
  });
277
72
  });
@@ -1,13 +1,15 @@
1
+ import { npmWhoAmI } from '@servicetitan/install';
1
2
  import chalk from 'chalk';
2
3
  import Table from 'cli-table3';
3
- import { fs, vol } from 'memfs';
4
+ import { vol } from 'memfs';
4
5
  import { createInterface } from 'readline/promises';
5
6
  import { formatRelativeDate, toArray } from '../../../utils';
6
- import { isTTY, npmView, npmWhoAmI, runCommand } from '../../utils';
7
+ import { isTTY, npmView, runCommand } from '../../utils';
7
8
  import { MFEList } from '../mfe-list';
8
9
 
10
+ jest.mock('@servicetitan/install');
9
11
  jest.mock('cli-table3');
10
- jest.mock('fs', () => fs);
12
+ jest.mock('fs', () => require('memfs').fs);
11
13
  jest.mock('readline/promises', () => ({
12
14
  createInterface: jest.fn(),
13
15
  }));
@@ -15,7 +17,6 @@ jest.mock('../../utils', () => ({
15
17
  ...jest.requireActual('../../utils'),
16
18
  isTTY: jest.fn(),
17
19
  npmView: jest.fn(),
18
- npmWhoAmI: jest.fn(),
19
20
  runCommand: jest.fn(),
20
21
  }));
21
22
 
@@ -1,17 +1,14 @@
1
+ import { isCI } from '@servicetitan/install';
1
2
  import { execSync } from 'child_process';
2
- import { vol, fs } from 'memfs';
3
+ import { vol } from 'memfs';
3
4
  import { inspect } from 'node:util';
4
5
  import path from 'path';
5
- import { isCI } from '../../utils';
6
6
  import { log } from '../../../utils';
7
7
  import { UploadSourcemaps } from '../upload-sourcemaps';
8
8
 
9
+ jest.mock('@servicetitan/install');
10
+ jest.mock('fs', () => require('memfs').fs);
9
11
  jest.mock('child_process', () => ({ execSync: jest.fn() }));
10
- jest.mock('fs', () => fs);
11
- jest.mock('../../utils', () => ({
12
- ...jest.requireActual('../../utils'),
13
- isCI: jest.fn(),
14
- }));
15
12
  jest.mock('../../../utils', () => ({
16
13
  ...jest.requireActual('../../../utils'),
17
14
  log: { info: jest.fn(), warning: jest.fn() },
@@ -1,8 +1,8 @@
1
+ import { gitCloneRepo, gitIsReachable } from '@servicetitan/install';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
4
 
4
5
  import { log, logErrors } from '../../utils';
5
- import { gitCloneRepo, gitIsReachable } from '../utils';
6
6
  import { Command, CommandArgs } from './types';
7
7
 
8
8
  interface Args extends CommandArgs {
@@ -28,7 +28,7 @@ export class Init extends Command<Args> {
28
28
  throw new Error(`${destination} is not an empty directory`);
29
29
  }
30
30
 
31
- if (await this.cloneRepo(destination)) {
31
+ if (this.cloneRepo(destination)) {
32
32
  log.info(`copied example project to ${destination}`);
33
33
  return;
34
34
  }
@@ -36,10 +36,12 @@ export class Init extends Command<Args> {
36
36
  if (!gitIsReachable({ name: REPO_NAME })) {
37
37
  throw new Error(`could not read servicetitan/${REPO_NAME} repository`);
38
38
  }
39
+
40
+ return Promise.resolve();
39
41
  }
40
42
 
41
- async cloneRepo(destination: string) {
42
- const ok = await gitCloneRepo({ destination, name: REPO_NAME });
43
+ cloneRepo(destination: string) {
44
+ const ok = gitCloneRepo({ destination, name: REPO_NAME });
43
45
  if (!ok) {
44
46
  return false;
45
47
  }
@@ -1,142 +1,39 @@
1
1
  import { execSync } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
- import { log, logErrors, getStartupVersion, readJsonSafe } from '../../utils';
2
+ import { log, logErrors, getStartupVersion } from '../../utils';
6
3
  import { Command, CommandArgs } from './types';
7
- import { gitCloneRepo, isCI, npmWhoAmI } from '../utils';
8
-
9
4
  interface Args extends CommandArgs {
10
5
  fix?: boolean;
11
6
  quiet?: boolean;
12
7
  token?: boolean;
13
8
  }
14
9
 
15
- const REPO_NAME = 'frontend-dev-config';
16
- const AUTH_TOKEN_KEY = '//registry.npmjs.org/:_authToken';
17
- const AUTH_TOKEN_REGEX = /^\/\/registry\.npmjs\.org\/:_authToken=\s*\${([^}]+)}/m;
18
-
19
10
  export class Install extends Command<Args> {
20
11
  static readonly description = 'Install project dependencies';
21
12
  static readonly options = {
22
13
  fix: { boolean: true, describe: 'Update and dedupe package-lock.json', hidden: true },
23
- quite: { boolean: true, describe: 'Suppress output', hidden: true },
14
+ quiet: { boolean: true, describe: 'Suppress output', hidden: true },
24
15
  token: { boolean: true, describe: 'Configure npm token' },
25
16
  };
26
17
 
27
18
  @logErrors
28
19
  async execute() {
29
- if (!this.args?.quiet) {
20
+ if (!this.args.quiet) {
30
21
  log.info(`startup cli v${getStartupVersion()}`);
31
22
  }
32
23
 
33
- const env = await this.configureNpmToken();
24
+ const options = [
25
+ this.args.fix ? '--fix' : '',
26
+ this.args.quiet ? '--quiet' : '',
27
+ this.args.token === true ? '--token' : '',
28
+ this.args.token === false ? '--no-token' : '',
29
+ ].filter(option => !!option);
34
30
 
35
- if (this.args?.token !== true) {
36
- this.installPackages(env);
37
- }
31
+ const command = `npx @servicetitan/install ${options.join(' ')}`.trim();
38
32
 
39
- return Promise.resolve(); // stops "async method has no 'await' expression" lint error
40
- }
41
-
42
- private async configureNpmToken() {
43
- if (isCI() || this.args?.fix || this.args?.token === false) {
44
- return;
45
- }
46
-
47
- if (!this.args?.quiet) {
48
- log.info('Configuring NPM token ...');
49
- }
33
+ log.debug('install', command);
50
34
 
51
- if (this.args?.token !== true) {
52
- const npmUser = npmWhoAmI();
53
- /* istanbul ignore next: debug only */
54
- log.debug('install:npm-user', () => JSON.stringify({ npmUser }));
55
- if (npmUser === 'st-team') {
56
- return;
57
- }
58
- }
59
-
60
- const token = await this.fetchNpmToken();
61
- if (!token) {
62
- return;
63
- }
64
-
65
- execSync(`npm config set "${AUTH_TOKEN_KEY}"="${token}"`);
66
-
67
- if (!fs.existsSync('.npmrc')) {
68
- return;
69
- }
70
-
71
- const npmrc = fs.readFileSync('.npmrc', 'utf-8');
72
- const match = AUTH_TOKEN_REGEX.exec(npmrc);
73
- if (match) {
74
- return { [match[1]]: token };
75
- }
35
+ execSync(command, { stdio: 'inherit' });
76
36
 
77
- execSync(`npm config delete --location=project "${AUTH_TOKEN_KEY}"`);
78
- }
79
-
80
- private async fetchNpmToken() {
81
- const tempDirPath = fs.mkdtempSync(path.join(os.tmpdir(), 'st-install-'));
82
- try {
83
- if (!(await gitCloneRepo({ destination: tempDirPath, name: REPO_NAME }))) {
84
- throw new Error(`could not clone servicetitan/${REPO_NAME}`);
85
- }
86
-
87
- const npmJson = readJsonSafe(path.join(tempDirPath, '.npm.json'));
88
-
89
- /* istanbul ignore next: debug only */
90
- log.debug('install:fetch-token', () => JSON.stringify(npmJson, null, 2));
91
-
92
- if (!((npmJson && typeof npmJson === 'object') || Array.isArray(npmJson))) {
93
- throw new Error('.npm.json is not an object');
94
- }
95
-
96
- const { readOnlyToken: authToken } = npmJson;
97
- if (!authToken) {
98
- throw new Error('.npm.json does not contain auth token');
99
- }
100
-
101
- if (typeof authToken !== 'string') {
102
- throw new Error('.npm.json auth token is not a string');
103
- }
104
-
105
- return Buffer.from(authToken, 'base64').toString('utf-8');
106
- } catch (e) {
107
- log.warning(String(e));
108
- } finally {
109
- try {
110
- fs.rmSync(tempDirPath, { recursive: true, force: true });
111
- } catch {
112
- // ignore
113
- }
114
- }
115
- }
116
-
117
- private installPackages(env?: Record<string, string>) {
118
- /* istanbul ignore next: debug only */
119
- log.debug('install:install-packages', () => JSON.stringify({ env }));
120
-
121
- /**
122
- * Note, if these are changed, update bootstrap.js to match
123
- * @see {@link file://./../../../../../bootstrap.js}
124
- */
125
- const npmArguments = [
126
- isCI() ? 'ci' : 'i',
127
- '--audit=false',
128
- '--fund=false',
129
- '--legacy-peer-deps',
130
- ...(this.args?.fix ? ['--package-lock-only', '--prefer-dedupe'] : []),
131
- ].join(' ');
132
-
133
- if (!this.args?.quiet) {
134
- log.info(`Running npm ${npmArguments} ...`);
135
- }
136
-
137
- execSync(`npm ${npmArguments}`, {
138
- ...(env ? { env: { ...process.env, ...env } } : {}),
139
- stdio: 'inherit',
140
- });
37
+ return Promise.resolve(); // stops "async method has no 'await' expression" lint error
141
38
  }
142
39
  }
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention */
2
+ import { npmWhoAmI } from '@servicetitan/install';
2
3
  import chalk from 'chalk';
3
4
  import Table from 'cli-table3';
4
5
  import readline from 'readline/promises';
@@ -11,7 +12,7 @@ import {
11
12
  PackageType,
12
13
  } from '../../utils';
13
14
  import { Command, CommandArgs } from './types';
14
- import { isTTY, NPMPackageInfo, npmView, npmWhoAmI, runCommand } from '../utils';
15
+ import { isTTY, NPMPackageInfo, npmView, runCommand } from '../utils';
15
16
 
16
17
  interface Args extends CommandArgs {
17
18
  _: string[];
@@ -0,0 +1,81 @@
1
+ import { execSync } from 'child_process';
2
+ import { FixCategory, Package, PackageError, ReviewConfiguration } from '../../types';
3
+ import { mockProject } from '../__mocks__';
4
+
5
+ import { NoDeprecatedStartupInstall } from '../no-deprecated-startup-install';
6
+
7
+ jest.mock('child_process', () => ({ execSync: jest.fn() }));
8
+
9
+ describe(`[startup] Review ${NoDeprecatedStartupInstall.name}`, () => {
10
+ const id = 'no-deprecated-startup-install';
11
+ const rule = new NoDeprecatedStartupInstall();
12
+ let config: ReviewConfiguration;
13
+ let pkg: Package;
14
+ let packages: Package[];
15
+
16
+ beforeEach(() => {
17
+ config = {};
18
+ pkg = { name: 'project', location: '.' };
19
+ packages = [pkg];
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ const subject = () => rule.run(mockProject({ config, packages }));
24
+
25
+ const fixSubject = () => rule.fix(subject()!);
26
+
27
+ function itReturnsNothing() {
28
+ test('returns nothing', () => {
29
+ expect(subject()).toBeUndefined();
30
+ });
31
+ }
32
+
33
+ function itReturnsError() {
34
+ test('returns error', () => {
35
+ expect(subject()).toEqual({
36
+ id,
37
+ message: 'project uses deprecated "@servicetitan/startup install" script',
38
+ location: pkg.location,
39
+ fixable: FixCategory.isolated,
40
+ } satisfies PackageError);
41
+ });
42
+ }
43
+
44
+ itReturnsNothing();
45
+
46
+ describe('when project uses deprecated bootstrap script', () => {
47
+ beforeEach(() => {
48
+ pkg.scripts = { bootstrap: 'npx --yes @servicetitan/startup install' };
49
+ });
50
+
51
+ itReturnsError();
52
+
53
+ test('fixes error', () => {
54
+ fixSubject();
55
+
56
+ expect(execSync).toHaveBeenCalledWith(
57
+ 'npm pkg set scripts.bootstrap="npx --yes @servicetitan/install"'
58
+ );
59
+ });
60
+
61
+ describe('when script includes version tag', () => {
62
+ beforeEach(() => {
63
+ pkg.scripts!.bootstrap = 'npx --yes @servicetitan/startup@30.0.0 install';
64
+ });
65
+
66
+ itReturnsError();
67
+ });
68
+ });
69
+
70
+ describe('with no root package', () => {
71
+ beforeEach(() => (packages = []));
72
+
73
+ itReturnsNothing();
74
+ });
75
+
76
+ test('ignores invalid error', () => {
77
+ rule.fix({} as any);
78
+
79
+ expect(execSync).not.toHaveBeenCalled();
80
+ });
81
+ });
@@ -1,5 +1,6 @@
1
1
  import { PackageRule } from '../types';
2
2
  import { NoDeprecatedContentBase } from './no-deprecated-content-base';
3
+ import { NoDeprecatedStartupInstall } from './no-deprecated-startup-install';
3
4
  import { NoDirectPeerDependencies } from './no-direct-peer-dependencies';
4
5
  import { NoTypescriptEntryPoint } from './no-typescript-entry-point';
5
6
  import { PreferOpenEndedPeerDependencies } from './prefer-open-ended-peer-dependencies';
@@ -24,6 +25,7 @@ export const rules: PackageRule[] = [
24
25
  new RequireCompatibleLaunchDarklySdk(),
25
26
  new NoDeprecatedContentBase(),
26
27
  new NoDirectPeerDependencies(),
28
+ new NoDeprecatedStartupInstall(),
27
29
  new NoTypescriptEntryPoint(),
28
30
  new PreferOpenEndedPeerDependencies(),
29
31
  new RequireServiceTitanScope(),
@@ -0,0 +1,30 @@
1
+ import { execSync } from 'child_process';
2
+ import { FixCategory, PackageError, PackageRule, Project } from '../types';
3
+
4
+ export class NoDeprecatedStartupInstall implements PackageRule {
5
+ get id() {
6
+ return 'no-deprecated-startup-install';
7
+ }
8
+
9
+ run(project: Project): PackageError | undefined {
10
+ const root = project.packages.find(({ location }) => location === '.');
11
+ const bootstrap = root?.scripts?.bootstrap;
12
+
13
+ if (bootstrap && /@servicetitan\/startup.* install/.test(bootstrap)) {
14
+ return {
15
+ id: this.id,
16
+ message: 'project uses deprecated "@servicetitan/startup install" script',
17
+ location: root.location,
18
+ fixable: FixCategory.isolated,
19
+ };
20
+ }
21
+ }
22
+
23
+ fix({ location }: PackageError) {
24
+ if (!location) {
25
+ return;
26
+ }
27
+
28
+ execSync('npm pkg set scripts.bootstrap="npx --yes @servicetitan/install"');
29
+ }
30
+ }