@servicetitan/startup 36.1.1 → 36.1.2-canary.2

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 (134) hide show
  1. package/bin/_run.js +77 -0
  2. package/bin/cpx.js +3 -0
  3. package/bin/eslint.js +3 -0
  4. package/bin/jest.js +3 -0
  5. package/bin/js-yaml.js +3 -0
  6. package/bin/json5.js +3 -0
  7. package/bin/kendo-ui-license.js +3 -0
  8. package/bin/lerna.js +3 -0
  9. package/bin/lessc.js +3 -0
  10. package/bin/patch-package.js +3 -0
  11. package/bin/prettier.js +3 -0
  12. package/bin/sass.js +3 -0
  13. package/bin/semver.js +3 -0
  14. package/bin/spack.js +3 -0
  15. package/bin/stylelint.js +3 -0
  16. package/bin/swc.js +3 -0
  17. package/bin/swcx.js +3 -0
  18. package/bin/tcm.js +3 -0
  19. package/bin/ts-jest.js +3 -0
  20. package/bin/ts-node-cwd.js +3 -0
  21. package/bin/ts-node-esm.js +3 -0
  22. package/bin/ts-node-script.js +3 -0
  23. package/bin/ts-node-transpile-only.js +3 -0
  24. package/bin/ts-node.js +3 -0
  25. package/bin/ts-script.js +3 -0
  26. package/bin/tsc.js +3 -0
  27. package/bin/tsserver.js +3 -0
  28. package/bin/vitest.js +3 -0
  29. package/bin/webpack-bundle-analyzer.js +3 -0
  30. package/bin/webpack-dev-server.js +3 -0
  31. package/bin/webpack.js +3 -0
  32. package/dist/cli/commands/clean.d.ts +1 -0
  33. package/dist/cli/commands/clean.d.ts.map +1 -1
  34. package/dist/cli/commands/clean.js +18 -8
  35. package/dist/cli/commands/clean.js.map +1 -1
  36. package/dist/cli/commands/coverage-finalize.d.ts +16 -0
  37. package/dist/cli/commands/coverage-finalize.d.ts.map +1 -0
  38. package/dist/cli/commands/coverage-finalize.js +41 -0
  39. package/dist/cli/commands/coverage-finalize.js.map +1 -0
  40. package/dist/cli/commands/install.js +1 -1
  41. package/dist/cli/commands/install.js.map +1 -1
  42. package/dist/cli/commands/kendo-ui-license.js +1 -2
  43. package/dist/cli/commands/kendo-ui-license.js.map +1 -1
  44. package/dist/cli/commands/prepare-package.js +2 -2
  45. package/dist/cli/commands/prepare-package.js.map +1 -1
  46. package/dist/cli/commands/registry/coverage-finalize.d.ts +5 -0
  47. package/dist/cli/commands/registry/coverage-finalize.d.ts.map +1 -0
  48. package/dist/cli/commands/registry/coverage-finalize.js +17 -0
  49. package/dist/cli/commands/registry/coverage-finalize.js.map +1 -0
  50. package/dist/cli/commands/review/review.d.ts.map +1 -1
  51. package/dist/cli/commands/review/review.js +8 -7
  52. package/dist/cli/commands/review/review.js.map +1 -1
  53. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.d.ts +0 -1
  54. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.d.ts.map +1 -1
  55. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.js +12 -18
  56. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.js.map +1 -1
  57. package/dist/cli/commands/review/rules/require-project-version-in-root-node-modules.d.ts.map +1 -1
  58. package/dist/cli/commands/review/rules/require-project-version-in-root-node-modules.js +9 -4
  59. package/dist/cli/commands/review/rules/require-project-version-in-root-node-modules.js.map +1 -1
  60. package/dist/cli/commands/review/types.d.ts +2 -8
  61. package/dist/cli/commands/review/types.d.ts.map +1 -1
  62. package/dist/cli/commands/review/types.js.map +1 -1
  63. package/dist/cli/utils/index.d.ts +0 -3
  64. package/dist/cli/utils/index.d.ts.map +1 -1
  65. package/dist/cli/utils/index.js +0 -3
  66. package/dist/cli/utils/index.js.map +1 -1
  67. package/dist/cypress/config/webpack-config.js +0 -1
  68. package/dist/cypress/config/webpack-config.js.map +1 -1
  69. package/dist/storybook-config/webpack-final.d.ts.map +1 -1
  70. package/dist/storybook-config/webpack-final.js +0 -6
  71. package/dist/storybook-config/webpack-final.js.map +1 -1
  72. package/dist/utils/default-excludes.d.ts +14 -0
  73. package/dist/utils/default-excludes.d.ts.map +1 -0
  74. package/dist/utils/default-excludes.js +23 -0
  75. package/dist/utils/default-excludes.js.map +1 -0
  76. package/dist/utils/find-packages.d.ts.map +1 -1
  77. package/dist/utils/find-packages.js +15 -0
  78. package/dist/utils/find-packages.js.map +1 -1
  79. package/dist/utils/get-coverage-aliases.d.ts +23 -0
  80. package/dist/utils/get-coverage-aliases.d.ts.map +1 -0
  81. package/dist/utils/get-coverage-aliases.js +41 -0
  82. package/dist/utils/get-coverage-aliases.js.map +1 -0
  83. package/dist/utils/get-coverage-source-patterns.d.ts +21 -0
  84. package/dist/utils/get-coverage-source-patterns.d.ts.map +1 -0
  85. package/dist/utils/get-coverage-source-patterns.js +39 -0
  86. package/dist/utils/get-coverage-source-patterns.js.map +1 -0
  87. package/dist/utils/get-jest-config.d.ts.map +1 -1
  88. package/dist/utils/get-jest-config.js +2 -1
  89. package/dist/utils/get-jest-config.js.map +1 -1
  90. package/dist/utils/prettify.js +1 -1
  91. package/dist/utils/prettify.js.map +1 -1
  92. package/dist/webpack/configs/cache-config.d.ts.map +1 -1
  93. package/dist/webpack/configs/cache-config.js +3 -1
  94. package/dist/webpack/configs/cache-config.js.map +1 -1
  95. package/dist/webpack/configs/rules/ts-coverage-rules.d.ts +22 -0
  96. package/dist/webpack/configs/rules/ts-coverage-rules.d.ts.map +1 -0
  97. package/dist/webpack/configs/rules/ts-coverage-rules.js +48 -0
  98. package/dist/webpack/configs/rules/ts-coverage-rules.js.map +1 -0
  99. package/eslint/config.mjs +2 -0
  100. package/package.json +54 -18
  101. package/src/__tests__/postinstall.test.ts +173 -0
  102. package/src/cli/commands/__tests__/clean.test.ts +30 -12
  103. package/src/cli/commands/__tests__/install.test.ts +3 -1
  104. package/src/cli/commands/__tests__/kendo-ui-license.test.ts +4 -4
  105. package/src/cli/commands/__tests__/prepare-package.test.ts +2 -3
  106. package/src/cli/commands/clean.ts +18 -9
  107. package/src/cli/commands/install.ts +1 -1
  108. package/src/cli/commands/kendo-ui-license.ts +1 -1
  109. package/src/cli/commands/prepare-package.ts +1 -1
  110. package/src/cli/commands/review/__mocks__/index.ts +1 -0
  111. package/src/cli/commands/review/__mocks__/mock-package-manager.ts +20 -0
  112. package/src/cli/commands/review/__tests__/review.test.ts +23 -31
  113. package/src/cli/commands/review/review.ts +11 -7
  114. package/src/cli/commands/review/rules/__mocks__/mock-project.ts +2 -1
  115. package/src/cli/commands/review/rules/__tests__/require-compatible-launch-darkly-sdk.test.ts +39 -49
  116. package/src/cli/commands/review/rules/__tests__/require-project-version-in-root-node-modules.test.ts +35 -22
  117. package/src/cli/commands/review/rules/require-compatible-launch-darkly-sdk.ts +13 -28
  118. package/src/cli/commands/review/rules/require-project-version-in-root-node-modules.ts +10 -3
  119. package/src/cli/commands/review/types.ts +3 -12
  120. package/src/cli/utils/index.ts +10 -3
  121. package/src/cypress/config/__tests__/webpack-config.test.ts +0 -9
  122. package/src/cypress/config/webpack-config.ts +0 -1
  123. package/src/postinstall.js +106 -14
  124. package/src/scripts/__tests__/generate-bin-wrappers.test.ts +226 -0
  125. package/src/scripts/generate-bin-wrappers.js +205 -0
  126. package/src/storybook-config/__tests__/webpack-final.test.ts +0 -25
  127. package/src/storybook-config/webpack-final.ts +0 -4
  128. package/src/utils/__tests__/get-jest-config.test.ts +3 -1
  129. package/src/utils/__tests__/get-packages.test.ts +47 -2
  130. package/src/utils/__tests__/prettify.test.ts +1 -1
  131. package/src/utils/find-packages.ts +16 -0
  132. package/src/utils/get-jest-config.ts +4 -1
  133. package/src/utils/prettify.ts +1 -1
  134. package/src/webpack/configs/cache-config.ts +3 -1
@@ -0,0 +1,173 @@
1
+ import { execFileSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import { vol } from 'memfs';
4
+ import path from 'path';
5
+
6
+ jest.mock('fs', () => require('memfs').fs);
7
+ jest.mock('child_process');
8
+
9
+ const { main } = require('../postinstall');
10
+
11
+ /*
12
+ * Node 22's path.resolve uses a native binding that bypasses jest.spyOn on
13
+ * process.cwd. Re-wire path.resolve so it picks up the mocked cwd.
14
+ */
15
+ const realResolve = path.resolve.bind(path);
16
+
17
+ describe('[startup] postinstall', () => {
18
+ const projectDir = '/project';
19
+ const nodeModulesDir = path.join(projectDir, 'node_modules');
20
+ const startupDir = path.join(nodeModulesDir, '@servicetitan', 'startup');
21
+ const patchDir = path.join(startupDir, 'patches');
22
+
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ jest.spyOn(process, 'cwd').mockReturnValue(startupDir);
26
+ jest.spyOn(path, 'resolve').mockImplementation((...args: string[]) =>
27
+ realResolve(process.cwd(), ...args)
28
+ );
29
+ process.env.INIT_CWD = projectDir;
30
+ });
31
+
32
+ afterEach(() => {
33
+ vol.reset();
34
+ delete process.env.INIT_CWD;
35
+ });
36
+
37
+ describe(main.name, () => {
38
+ const pkg = { name: 'jsdom', version: '26.1.0' };
39
+
40
+ beforeEach(() => {
41
+ vol.fromJSON({
42
+ [path.join(nodeModulesDir, pkg.name, 'index.js')]: '',
43
+ [path.join(startupDir, 'package.json')]: '{}',
44
+ [path.join(patchDir, `${pkg.name}+${pkg.version}.patch`)]: '',
45
+ });
46
+ });
47
+
48
+ const subject = () => main();
49
+
50
+ function itRunsPatchPackage() {
51
+ test('runs patch-package', () => {
52
+ subject();
53
+
54
+ expect(execFileSync).toHaveBeenCalledWith(
55
+ 'patch-package',
56
+ ['--error-on-fail', '--patch-dir', path.relative(projectDir, patchDir)],
57
+ expect.objectContaining({ cwd: projectDir })
58
+ );
59
+ });
60
+ }
61
+
62
+ function itDoesNothing() {
63
+ test('does not run patch-package', () => {
64
+ subject();
65
+
66
+ expect(execFileSync).not.toHaveBeenCalled();
67
+ });
68
+ }
69
+
70
+ itRunsPatchPackage();
71
+
72
+ describe('when package.json does not exist', () => {
73
+ beforeEach(() => fs.rmSync(path.join(startupDir, 'package.json')));
74
+
75
+ itDoesNothing();
76
+ });
77
+
78
+ describe('when patches are outside project directory', () => {
79
+ beforeEach(() => (process.env.INIT_CWD = '/other'));
80
+
81
+ itDoesNothing();
82
+ });
83
+
84
+ describe('with pnpm project', () => {
85
+ const npmPath = path.join(nodeModulesDir, pkg.name);
86
+ const pnpmStore = path.join(nodeModulesDir, '.pnpm');
87
+ const pnpmPath = path.join(
88
+ pnpmStore,
89
+ `${pkg.name}@${pkg.version}`,
90
+ 'node_modules',
91
+ pkg.name
92
+ );
93
+ let createdSymlink: boolean;
94
+
95
+ beforeEach(() => {
96
+ createdSymlink = false;
97
+ fs.rmSync(npmPath, { recursive: true });
98
+ fs.mkdirSync(pnpmPath, { recursive: true });
99
+ fs.writeFileSync(path.join(pnpmPath, 'index.js'), '');
100
+ jest.mocked(execFileSync).mockImplementation((): any => {
101
+ createdSymlink = fs.existsSync(npmPath);
102
+ });
103
+ });
104
+
105
+ itRunsPatchPackage();
106
+
107
+ test('creates transient symlink for patch-package', () => {
108
+ subject();
109
+
110
+ expect(createdSymlink).toBe(true);
111
+ expect(fs.existsSync(npmPath)).toBe(false);
112
+ });
113
+
114
+ describe('when package already exists in node_modules', () => {
115
+ beforeEach(() => {
116
+ fs.mkdirSync(npmPath, { recursive: true });
117
+ fs.writeFileSync(path.join(npmPath, 'index.js'), '');
118
+ });
119
+
120
+ test('does not replace it with a symlink', () => {
121
+ subject();
122
+
123
+ expect(fs.lstatSync(npmPath).isSymbolicLink()).toBe(false);
124
+ });
125
+ });
126
+
127
+ function itDoesNotCreateSymlink() {
128
+ test('does not create symlink', () => {
129
+ subject();
130
+
131
+ expect(createdSymlink).toBe(false);
132
+ });
133
+ }
134
+
135
+ describe('when package is not in .pnpm store', () => {
136
+ beforeEach(() => {
137
+ fs.rmSync(path.join(pnpmStore, `${pkg.name}@${pkg.version}`), {
138
+ recursive: true,
139
+ });
140
+ });
141
+
142
+ itDoesNotCreateSymlink();
143
+ });
144
+
145
+ describe('when patch filename is not valid', () => {
146
+ beforeEach(() => {
147
+ fs.renameSync(
148
+ path.join(patchDir, `${pkg.name}+${pkg.version}.patch`),
149
+ path.join(patchDir, `${pkg.name}.patch`)
150
+ );
151
+ });
152
+
153
+ itDoesNotCreateSymlink();
154
+ });
155
+
156
+ describe('when patch-package throws', () => {
157
+ beforeEach(() => {
158
+ jest.mocked(execFileSync).mockImplementation(() => {
159
+ createdSymlink = fs.existsSync(npmPath);
160
+ throw new Error('patch failed');
161
+ });
162
+ });
163
+
164
+ test('removes symlink', () => {
165
+ expect(() => subject()).toThrow();
166
+
167
+ expect(createdSymlink).toBe(true);
168
+ expect(fs.existsSync(npmPath)).toBe(false);
169
+ });
170
+ });
171
+ });
172
+ });
173
+ });
@@ -1,9 +1,11 @@
1
+ import { getPackageManager } from '@servicetitan/install';
1
2
  import { fs, vol } from 'memfs';
2
3
  import { exec, ExecException, execSync } from 'node:child_process';
3
4
  import { log } from '../../../utils';
4
5
  import { Clean } from '../clean';
5
6
  import { entry } from '../registry/clean';
6
7
 
8
+ jest.mock('@servicetitan/install', () => ({ getPackageManager: jest.fn() }));
7
9
  jest.mock('node:fs', () => fs);
8
10
  jest.mock('node:child_process', () => ({
9
11
  execSync: jest.fn(),
@@ -15,6 +17,7 @@ type ExecCallback = (error: ExecException | null, stdout: string, stderr: string
15
17
  describe(`[startup] ${Clean.name}`, () => {
16
18
  const cachePath = '/test-npm-cache';
17
19
  const npxCacheDirectoryName = '_npx';
20
+ const packageManager: Partial<ReturnType<typeof getPackageManager>> = { clearCache: jest.fn() };
18
21
 
19
22
  beforeEach(() => {
20
23
  jest.clearAllMocks();
@@ -23,6 +26,7 @@ describe(`[startup] ${Clean.name}`, () => {
23
26
  jest.mocked(execSync).mockImplementation((cmd: string) =>
24
27
  cmd === 'npm config get cache' ? cachePath : ''
25
28
  );
29
+ jest.mocked(getPackageManager).mockReturnValue(packageManager as any);
26
30
  // @ts-expect-error this is a valid exec overload but TS shows an error
27
31
  jest.mocked(exec).mockImplementation((_cmd: string, callback?: ExecCallback) => {
28
32
  if (callback) {
@@ -77,14 +81,17 @@ describe(`[startup] ${Clean.name}`, () => {
77
81
  });
78
82
  }
79
83
 
80
- test.each(['npx nx reset', 'npx jest --clearCache', 'npm cache clear --force'])(
81
- 'runs "%s"',
82
- async command => {
83
- await subject();
84
+ test.each(['nx reset', 'jest --clearCache'])('runs "%s"', async command => {
85
+ await subject();
86
+
87
+ expect(exec).toHaveBeenCalledWith(command, expect.any(Function));
88
+ });
84
89
 
85
- expect(exec).toHaveBeenCalledWith(command, expect.any(Function));
86
- }
87
- );
90
+ test('clears package manager cache', async () => {
91
+ await subject();
92
+
93
+ expect(packageManager.clearCache).toHaveBeenCalled();
94
+ });
88
95
 
89
96
  itRunsGitClean();
90
97
 
@@ -94,22 +101,33 @@ describe(`[startup] ${Clean.name}`, () => {
94
101
  expect(vol.existsSync(`${cachePath}/${npxCacheDirectoryName}`)).toBe(false);
95
102
  });
96
103
 
97
- describe('when "npx nx reset" fails', () => {
104
+ describe('when "nx reset" fails', () => {
98
105
  beforeEach(() => {
99
- failExecOn('npx nx reset');
106
+ failExecOn('nx reset');
100
107
  });
101
108
 
102
- itLogsError('Error running npx nx reset');
109
+ itLogsError('Error running nx reset');
103
110
  itRunsGitClean();
104
111
 
105
112
  test('runs remaining commands', async () => {
106
113
  await subject();
107
114
 
108
- expect(exec).toHaveBeenCalledWith('npx jest --clearCache', expect.any(Function));
109
- expect(exec).toHaveBeenCalledWith('npm cache clear --force', expect.any(Function));
115
+ expect(exec).toHaveBeenCalledWith('jest --clearCache', expect.any(Function));
116
+ expect(packageManager.clearCache).toHaveBeenCalled();
110
117
  });
111
118
  });
112
119
 
120
+ describe('when clearing package manager cache fails', () => {
121
+ beforeEach(() => {
122
+ jest.spyOn(packageManager, 'clearCache').mockImplementation(() => {
123
+ throw new Error('Oops!');
124
+ });
125
+ });
126
+
127
+ itLogsError('Error clearing package manager cache');
128
+ itRunsGitClean();
129
+ });
130
+
113
131
  describe('when clearing npx cache fails', () => {
114
132
  beforeEach(() => {
115
133
  jest.mocked(execSync).mockImplementation(cmd => {
@@ -29,7 +29,9 @@ describe(`${Install.name}`, () => {
29
29
  test('runs install', async () => {
30
30
  await subject();
31
31
 
32
- expect(execSync).toHaveBeenCalledWith(`npx @servicetitan/install`, { stdio: 'inherit' });
32
+ expect(execSync).toHaveBeenCalledWith(`npx --yes @servicetitan/install`, {
33
+ stdio: 'inherit',
34
+ });
33
35
  });
34
36
 
35
37
  const keys = ['fix', 'quiet', 'token'] satisfies (keyof typeof args)[];
@@ -33,8 +33,8 @@ describe(`${kendoUILicense.name}`, () => {
33
33
 
34
34
  expect(log.info).toHaveBeenCalledWith('Activating Kendo UI with embedded license...');
35
35
  expect(execa).toHaveBeenCalledWith(
36
- 'npx',
37
- ['kendo-ui-license', 'activate', '--no-install'],
36
+ 'kendo-ui-license',
37
+ ['activate', '--no-install'],
38
38
  expect.objectContaining({
39
39
  // eslint-disable-next-line @typescript-eslint/naming-convention
40
40
  env: { KENDO_UI_LICENSE: expect.anything() },
@@ -54,8 +54,8 @@ describe(`${kendoUILicense.name}`, () => {
54
54
  'Activating Kendo UI with license from environment...'
55
55
  );
56
56
  expect(execa).toHaveBeenCalledWith(
57
- 'npx',
58
- ['kendo-ui-license', 'activate', '--no-install'],
57
+ 'kendo-ui-license',
58
+ ['activate', '--no-install'],
59
59
  expect.objectContaining({ env: undefined })
60
60
  );
61
61
  });
@@ -1,8 +1,7 @@
1
- import { copyFiles } from '../../utils';
1
+ import { copyFiles } from '../../utils/copy-files';
2
2
  import { PreparePackage } from '../prepare-package';
3
3
 
4
- jest.mock('../../utils', () => ({
5
- ...jest.requireActual('../../utils'),
4
+ jest.mock('../../utils/copy-files', () => ({
6
5
  copyFiles: jest.fn(),
7
6
  }));
8
7
 
@@ -1,3 +1,4 @@
1
+ import { getPackageManager } from '@servicetitan/install';
1
2
  import { exec as execAsync, execSync } from 'node:child_process';
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
@@ -10,11 +11,11 @@ import { Command } from './types';
10
11
  const exec = promisify(execAsync);
11
12
 
12
13
  enum CleanProcesses {
13
- ResetNx,
14
+ CleanGitWorkingTree,
14
15
  ClearJestCache,
15
- ClearNpmCache,
16
16
  ClearNpxCache,
17
- CleanGitWorkingTree,
17
+ ClearPackageManagerCache,
18
+ ResetNx,
18
19
  }
19
20
 
20
21
  export class Clean extends Command<typeof entry> {
@@ -22,12 +23,10 @@ export class Clean extends Command<typeof entry> {
22
23
  async execute() {
23
24
  const processTree = new ProcessTree<typeof CleanProcesses>();
24
25
 
25
- processTree.add(CleanProcesses.ResetNx, () => this.runCommand('npx nx reset'));
26
- processTree.add(CleanProcesses.ClearJestCache, () =>
27
- this.runCommand('npx jest --clearCache')
28
- );
29
- processTree.add(CleanProcesses.ClearNpmCache, () =>
30
- this.runCommand('npm cache clear --force')
26
+ processTree.add(CleanProcesses.ResetNx, () => this.runCommand('nx reset'));
27
+ processTree.add(CleanProcesses.ClearJestCache, () => this.runCommand('jest --clearCache'));
28
+ processTree.add(CleanProcesses.ClearPackageManagerCache, () =>
29
+ Promise.resolve(this.clearPackageManagerCache())
31
30
  );
32
31
  processTree.add(CleanProcesses.ClearNpxCache, () => this.clearNpxCache());
33
32
 
@@ -63,4 +62,14 @@ export class Clean extends Command<typeof entry> {
63
62
  log.error(`Error clearing npx cache: ${error}`);
64
63
  }
65
64
  }
65
+
66
+ private clearPackageManagerCache() {
67
+ try {
68
+ const packageManager = getPackageManager();
69
+ log.info(`Clearing ${packageManager.command} cache`);
70
+ packageManager.clearCache();
71
+ } catch (error) {
72
+ log.error(`Error clearing package manager cache: ${error}`);
73
+ }
74
+ }
66
75
  }
@@ -17,7 +17,7 @@ export class Install extends Command<typeof entry> {
17
17
  this.args.token === false ? '--no-token' : '',
18
18
  ].filter(option => !!option);
19
19
 
20
- const command = `npx @servicetitan/install ${options.join(' ')}`.trim();
20
+ const command = `npx --yes @servicetitan/install ${options.join(' ')}`.trim();
21
21
 
22
22
  log.debug('install', command);
23
23
 
@@ -51,7 +51,7 @@ export class KendoUILicense extends Command<typeof entry> {
51
51
  );
52
52
 
53
53
  try {
54
- await execa('npx', ['kendo-ui-license', 'activate', '--no-install'], {
54
+ await execa('kendo-ui-license', ['activate', '--no-install'], {
55
55
  env,
56
56
  stdio: 'inherit',
57
57
  });
@@ -1,5 +1,5 @@
1
1
  import { logErrors } from '../../utils';
2
- import { copyFiles } from '../utils';
2
+ import { copyFiles } from '../utils/copy-files';
3
3
  import type { entry } from './registry/prepare-package';
4
4
  import { Command } from './types';
5
5
 
@@ -1 +1,2 @@
1
1
  export * from './expect-calls';
2
+ export * from './mock-package-manager';
@@ -0,0 +1,20 @@
1
+ import type { DeclaredVersionQuery, ResolvedVersionQuery } from '@servicetitan/install';
2
+
3
+ export class MockPackageManager {
4
+ command = 'test';
5
+ lockFileName = 'test-lock.json';
6
+
7
+ getDeclaredVersion(_query: DeclaredVersionQuery): string | undefined {
8
+ return undefined;
9
+ }
10
+
11
+ getResolvedVersion(_query: ResolvedVersionQuery): string | undefined {
12
+ return undefined;
13
+ }
14
+
15
+ hasValidLockFile(): boolean {
16
+ return true;
17
+ }
18
+
19
+ installPackages() {}
20
+ }
@@ -1,18 +1,18 @@
1
+ import { getPackageManager, PackageManager } from '@servicetitan/install';
1
2
  import chalk from 'chalk';
2
- import { execSync } from 'child_process';
3
3
  import { fs, vol } from 'memfs';
4
4
  import path from 'path';
5
5
  import terminalLink from 'terminal-link';
6
6
  import type { ProjectPackage } from '../../../../utils';
7
7
  import { findPackages, getReviewConfiguration, log } from '../../../../utils';
8
8
  import { entry } from '../../registry/review';
9
- import { expectCalls } from '../__mocks__';
9
+ import { expectCalls, MockPackageManager } from '../__mocks__';
10
10
  import { Review } from '../review';
11
11
  import { rules } from '../rules';
12
12
  import { ErrorSeverity, FixCategory, Level, PackageError, ReviewConfiguration } from '../types';
13
13
  import { indent } from '../utils';
14
14
 
15
- jest.mock('child_process', () => ({ execSync: jest.fn() }));
15
+ jest.mock('@servicetitan/install', () => ({ getPackageManager: jest.fn() }));
16
16
  jest.mock('fs', () => fs);
17
17
  jest.mock('terminal-link', () => jest.fn());
18
18
  jest.mock('../../../../utils', () => ({
@@ -73,7 +73,6 @@ describe(`[startup] ${Review.name}`, () => {
73
73
 
74
74
  describe('when run from workspace root', () => {
75
75
  const rootPackageJSON = { name: 'root', workspaces: ['packages/*'] };
76
- const packageLockJSON = { packages: {} };
77
76
  const packages: ProjectPackage[] = [
78
77
  {
79
78
  name: 'a',
@@ -96,11 +95,12 @@ describe(`[startup] ${Review.name}`, () => {
96
95
  foo: { '1.0.0': ['b'], '1.0.1': ['a'] },
97
96
  };
98
97
  let config: ReviewConfiguration;
98
+ let packageManager: PackageManager;
99
99
 
100
100
  function project() {
101
101
  return {
102
102
  config: {},
103
- packageLock: { ...packageLockJSON, location: './package-lock.json' },
103
+ packageManager,
104
104
  packages: [{ ...rootPackageJSON, location: '.' }, ...packages],
105
105
  dependencies: expectedDependencies,
106
106
  };
@@ -108,12 +108,15 @@ describe(`[startup] ${Review.name}`, () => {
108
108
 
109
109
  beforeEach(() => {
110
110
  config = {};
111
+ packageManager = new MockPackageManager() as any;
111
112
  vol.fromJSON({
112
113
  'package.json': JSON.stringify(rootPackageJSON),
113
- 'package-lock.json': JSON.stringify(packageLockJSON),
114
+ [packageManager.lockFileName]: JSON.stringify({}),
114
115
  });
115
116
  jest.resetAllMocks();
117
+ jest.spyOn(packageManager, 'installPackages');
116
118
  jest.mocked(findPackages).mockImplementation(() => packages);
119
+ jest.mocked(getPackageManager).mockReturnValue(packageManager);
117
120
  jest.mocked(getReviewConfiguration).mockImplementation(() => config);
118
121
  });
119
122
 
@@ -133,21 +136,10 @@ describe(`[startup] ${Review.name}`, () => {
133
136
 
134
137
  itReportsNoErrors();
135
138
 
136
- describe('with no package-lock.json', () => {
137
- beforeEach(() => fs.rmSync('package-lock.json'));
139
+ describe('when lock file is missing or invalid', () => {
140
+ beforeEach(() => jest.spyOn(packageManager, 'hasValidLockFile').mockReturnValue(false));
138
141
 
139
- itThrowsError(/must be run with valid package-lock\.json/);
140
- });
141
-
142
- describe('when package-lock.json omits "packages"', () => {
143
- beforeEach(() => {
144
- vol.fromJSON({
145
- 'package.json': JSON.stringify(rootPackageJSON),
146
- 'package-lock.json': JSON.stringify({}),
147
- });
148
- });
149
-
150
- itThrowsError(/must be run with valid package-lock\.json/);
142
+ itThrowsError(/missing or invalid lock file/);
151
143
  });
152
144
 
153
145
  describe('with --rule option', () => {
@@ -277,8 +269,8 @@ describe(`[startup] ${Review.name}`, () => {
277
269
  test('regenerates lockfile', async () => {
278
270
  await subject();
279
271
 
280
- expect(execSync).toHaveBeenCalledWith(
281
- 'npx @servicetitan/install --fix --quiet',
272
+ expect(packageManager.installPackages).toHaveBeenCalledWith(
273
+ { fix: true },
282
274
  { stdio: 'inherit' }
283
275
  );
284
276
  });
@@ -349,10 +341,10 @@ describe(`[startup] ${Review.name}`, () => {
349
341
  expectOutput(location(error), message(error, { indent: { label: 1 } }));
350
342
  });
351
343
 
352
- describe('when location is package-lock.json', () => {
353
- beforeEach(() => (error.location = project().packageLock.location));
344
+ describe('when location is lock file', () => {
345
+ beforeEach(() => (error.location = project().packageManager.lockFileName));
354
346
 
355
- test('outputs package-lock.json as location', async () => {
347
+ test('outputs lock file as location', async () => {
356
348
  await subject();
357
349
 
358
350
  expectOutput(chalk.underline(path.relative('.', error.location!)));
@@ -450,19 +442,19 @@ describe(`[startup] ${Review.name}`, () => {
450
442
  ]);
451
443
  };
452
444
 
453
- const execStartupInstallWithFixOption = [
454
- execSync,
455
- 'npx @servicetitan/install --fix --quiet',
445
+ const installPackagesWithFixOption = [
446
+ packageManager.installPackages,
447
+ { fix: true },
456
448
  { stdio: 'inherit' },
457
449
  ];
458
450
 
459
451
  expectCalls(
460
452
  ...fixesForCategory(FixCategory.isolated),
461
- execStartupInstallWithFixOption,
453
+ installPackagesWithFixOption,
462
454
  ...fixesForCategory(FixCategory.normal),
463
- execStartupInstallWithFixOption,
455
+ installPackagesWithFixOption,
464
456
  ...fixesForCategory(FixCategory.lockFile),
465
- execStartupInstallWithFixOption
457
+ installPackagesWithFixOption
466
458
  );
467
459
  });
468
460
 
@@ -1,5 +1,5 @@
1
+ import { getPackageManager } from '@servicetitan/install';
1
2
  import chalk from 'chalk';
2
- import { execSync } from 'child_process';
3
3
  import terminalLink from 'terminal-link';
4
4
  import {
5
5
  findPackages,
@@ -57,13 +57,12 @@ export class Review extends Command<typeof entry> {
57
57
  throw new Error('this command must be run from the workspace root directory');
58
58
  }
59
59
 
60
- const packageLock = readJsonSafe<Project['packageLock']>('package-lock.json');
61
- if (!packageLock?.packages) {
62
- throw new Error('this command must be run with valid package-lock.json');
60
+ const packageManager = getPackageManager();
61
+ if (!packageManager.hasValidLockFile()) {
62
+ throw new Error(`missing or invalid lock file: ${packageManager.lockFileName}`);
63
63
  }
64
64
 
65
65
  root.location = '.'; // identifies the root package.json for rules that care
66
- packageLock.location = './package-lock.json';
67
66
 
68
67
  const config = getReviewConfiguration();
69
68
  const packages: Package[] = [root, ...findPackages()];
@@ -71,7 +70,12 @@ export class Review extends Command<typeof entry> {
71
70
  this.debug('review:packages', packages);
72
71
  this.debug('review:dependencies', dependencies);
73
72
 
74
- return { config, dependencies, packageLock, packages };
73
+ return {
74
+ config,
75
+ dependencies,
76
+ packageManager,
77
+ packages,
78
+ };
75
79
  }
76
80
 
77
81
  private debug(namespace: string, data: any) {
@@ -110,7 +114,7 @@ export class Review extends Command<typeof entry> {
110
114
  }
111
115
  });
112
116
 
113
- execSync('npx @servicetitan/install --fix --quiet', { stdio: 'inherit' });
117
+ project.packageManager.installPackages({ fix: true }, { stdio: 'inherit' });
114
118
  project = this.createProject();
115
119
  errors = this.findErrors(project);
116
120
  }
@@ -1,10 +1,11 @@
1
+ import { MockPackageManager } from '../../__mocks__';
1
2
  import { Project } from '../../types';
2
3
 
3
4
  export function mockProject(project: Partial<Project>): Project {
4
5
  return {
5
6
  config: {},
6
7
  dependencies: {},
7
- packageLock: { packages: {}, location: './package-lock.json' },
8
+ packageManager: new MockPackageManager() as any,
8
9
  packages: [],
9
10
  ...project,
10
11
  };