@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.
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +6 -5
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/install.d.ts +1 -4
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/install.js +12 -118
- package/dist/cli/commands/install.js.map +1 -1
- package/dist/cli/commands/mfe-list.d.ts.map +1 -1
- package/dist/cli/commands/mfe-list.js +2 -1
- package/dist/cli/commands/mfe-list.js.map +1 -1
- package/dist/cli/commands/review/rules/index.d.ts.map +1 -1
- package/dist/cli/commands/review/rules/index.js +2 -0
- package/dist/cli/commands/review/rules/index.js.map +1 -1
- package/dist/cli/commands/review/rules/no-deprecated-startup-install.d.ts +7 -0
- package/dist/cli/commands/review/rules/no-deprecated-startup-install.d.ts.map +1 -0
- package/dist/cli/commands/review/rules/no-deprecated-startup-install.js +38 -0
- package/dist/cli/commands/review/rules/no-deprecated-startup-install.js.map +1 -0
- package/dist/cli/commands/review/types.d.ts +1 -0
- package/dist/cli/commands/review/types.d.ts.map +1 -1
- package/dist/cli/commands/review/types.js.map +1 -1
- package/dist/cli/commands/upload-sourcemaps.d.ts.map +1 -1
- package/dist/cli/commands/upload-sourcemaps.js +2 -1
- package/dist/cli/commands/upload-sourcemaps.js.map +1 -1
- package/dist/cli/utils/cli-git.d.ts +0 -9
- package/dist/cli/utils/cli-git.d.ts.map +1 -1
- package/dist/cli/utils/cli-git.js +0 -59
- package/dist/cli/utils/cli-git.js.map +1 -1
- package/dist/cli/utils/cli-npm.d.ts +0 -3
- package/dist/cli/utils/cli-npm.d.ts.map +1 -1
- package/dist/cli/utils/cli-npm.js +0 -22
- package/dist/cli/utils/cli-npm.js.map +1 -1
- package/dist/cli/utils/index.d.ts +0 -1
- package/dist/cli/utils/index.d.ts.map +1 -1
- package/dist/cli/utils/index.js +0 -1
- package/dist/cli/utils/index.js.map +1 -1
- package/dist/cli/utils/lerna-exec.d.ts.map +1 -1
- package/dist/cli/utils/lerna-exec.js +2 -2
- package/dist/cli/utils/lerna-exec.js.map +1 -1
- package/package.json +11 -8
- package/src/cli/commands/__tests__/init.test.ts +11 -14
- package/src/cli/commands/__tests__/install.test.ts +19 -224
- package/src/cli/commands/__tests__/mfe-list.test.ts +5 -4
- package/src/cli/commands/__tests__/upload-sourcemaps.test.ts +4 -7
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/install.ts +13 -116
- package/src/cli/commands/mfe-list.ts +2 -1
- package/src/cli/commands/review/rules/__tests__/no-deprecated-startup-install.test.ts +81 -0
- package/src/cli/commands/review/rules/index.ts +2 -0
- package/src/cli/commands/review/rules/no-deprecated-startup-install.ts +30 -0
- package/src/cli/commands/review/types.ts +1 -0
- package/src/cli/commands/upload-sourcemaps.ts +2 -1
- package/src/cli/utils/__tests__/cli-git.test.ts +4 -140
- package/src/cli/utils/__tests__/cli-npm.test.ts +0 -43
- package/src/cli/utils/__tests__/lerna-exec.test.ts +2 -2
- package/src/cli/utils/cli-git.ts +1 -52
- package/src/cli/utils/cli-npm.ts +0 -12
- package/src/cli/utils/index.ts +0 -1
- package/src/cli/utils/lerna-exec.ts +1 -1
- package/dist/cli/utils/is-ci.d.ts +0 -2
- package/dist/cli/utils/is-ci.d.ts.map +0 -1
- package/dist/cli/utils/is-ci.js +0 -15
- package/dist/cli/utils/is-ci.js.map +0 -1
- package/src/cli/utils/__tests__/is-ci.test.ts +0 -40
- 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()
|
|
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('
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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).
|
|
123
|
-
expect.
|
|
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
|
|
171
|
-
beforeEach(() =>
|
|
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(
|
|
47
|
+
test('runs install with --no-token', async () => {
|
|
206
48
|
await subject();
|
|
207
49
|
|
|
208
50
|
expect(execSync).toHaveBeenCalledWith(
|
|
209
|
-
|
|
51
|
+
expect.stringContaining('--no-token'),
|
|
210
52
|
expect.anything()
|
|
211
53
|
);
|
|
212
54
|
});
|
|
213
|
-
|
|
214
|
-
itDoesNotConfigureNpmToken();
|
|
215
55
|
});
|
|
216
56
|
|
|
217
|
-
|
|
218
|
-
|
|
57
|
+
test('logs progress', async () => {
|
|
58
|
+
await subject();
|
|
219
59
|
|
|
220
|
-
|
|
60
|
+
expect(log.info).toHaveBeenCalledWith(`startup cli v${startupVersion}`);
|
|
221
61
|
});
|
|
222
62
|
|
|
223
|
-
describe('with --
|
|
224
|
-
beforeEach(() => (args = {
|
|
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
|
-
|
|
237
|
-
test('logs error', async () => {
|
|
66
|
+
test('does not log progress', async () => {
|
|
238
67
|
await subject();
|
|
239
68
|
|
|
240
|
-
expect(log.
|
|
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 {
|
|
4
|
+
import { vol } from 'memfs';
|
|
4
5
|
import { createInterface } from 'readline/promises';
|
|
5
6
|
import { formatRelativeDate, toArray } from '../../../utils';
|
|
6
|
-
import { isTTY, npmView,
|
|
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
|
|
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() },
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
42
|
-
const ok =
|
|
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
|
|
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
|
-
|
|
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
|
|
20
|
+
if (!this.args.quiet) {
|
|
30
21
|
log.info(`startup cli v${getStartupVersion()}`);
|
|
31
22
|
}
|
|
32
23
|
|
|
33
|
-
const
|
|
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
|
-
|
|
36
|
-
this.installPackages(env);
|
|
37
|
-
}
|
|
31
|
+
const command = `npx @servicetitan/install ${options.join(' ')}`.trim();
|
|
38
32
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
}
|