@servicetitan/startup 24.0.3 → 24.1.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 (110) hide show
  1. package/dist/cli/commands/build.d.ts +1 -0
  2. package/dist/cli/commands/build.d.ts.map +1 -1
  3. package/dist/cli/commands/build.js +3 -0
  4. package/dist/cli/commands/build.js.map +1 -1
  5. package/dist/cli/commands/bundle-package.d.ts +1 -0
  6. package/dist/cli/commands/bundle-package.d.ts.map +1 -1
  7. package/dist/cli/commands/bundle-package.js +3 -0
  8. package/dist/cli/commands/bundle-package.js.map +1 -1
  9. package/dist/cli/commands/eslint.d.ts +1 -0
  10. package/dist/cli/commands/eslint.d.ts.map +1 -1
  11. package/dist/cli/commands/eslint.js +3 -0
  12. package/dist/cli/commands/eslint.js.map +1 -1
  13. package/dist/cli/commands/get-command.d.ts +6 -0
  14. package/dist/cli/commands/get-command.d.ts.map +1 -0
  15. package/dist/cli/commands/get-command.js +50 -0
  16. package/dist/cli/commands/get-command.js.map +1 -0
  17. package/dist/cli/commands/get-user-commands.d.ts +7 -0
  18. package/dist/cli/commands/get-user-commands.d.ts.map +1 -0
  19. package/dist/cli/commands/get-user-commands.js +17 -0
  20. package/dist/cli/commands/get-user-commands.js.map +1 -0
  21. package/dist/cli/commands/index.d.ts +3 -3
  22. package/dist/cli/commands/index.d.ts.map +1 -1
  23. package/dist/cli/commands/index.js +3 -0
  24. package/dist/cli/commands/index.js.map +1 -1
  25. package/dist/cli/commands/init.d.ts +2 -1
  26. package/dist/cli/commands/init.d.ts.map +1 -1
  27. package/dist/cli/commands/init.js +5 -2
  28. package/dist/cli/commands/init.js.map +1 -1
  29. package/dist/cli/commands/install.d.ts +1 -0
  30. package/dist/cli/commands/install.d.ts.map +1 -1
  31. package/dist/cli/commands/install.js +3 -0
  32. package/dist/cli/commands/install.js.map +1 -1
  33. package/dist/cli/commands/kendo-ui-license.d.ts +1 -0
  34. package/dist/cli/commands/kendo-ui-license.d.ts.map +1 -1
  35. package/dist/cli/commands/kendo-ui-license.js +3 -0
  36. package/dist/cli/commands/kendo-ui-license.js.map +1 -1
  37. package/dist/cli/commands/lint.d.ts +1 -0
  38. package/dist/cli/commands/lint.d.ts.map +1 -1
  39. package/dist/cli/commands/lint.js +3 -0
  40. package/dist/cli/commands/lint.js.map +1 -1
  41. package/dist/cli/commands/mfe-publish.d.ts +7 -2
  42. package/dist/cli/commands/mfe-publish.d.ts.map +1 -1
  43. package/dist/cli/commands/mfe-publish.js +25 -11
  44. package/dist/cli/commands/mfe-publish.js.map +1 -1
  45. package/dist/cli/commands/prepare-package.d.ts +1 -0
  46. package/dist/cli/commands/prepare-package.d.ts.map +1 -1
  47. package/dist/cli/commands/prepare-package.js +3 -0
  48. package/dist/cli/commands/prepare-package.js.map +1 -1
  49. package/dist/cli/commands/start.d.ts +1 -0
  50. package/dist/cli/commands/start.d.ts.map +1 -1
  51. package/dist/cli/commands/start.js +3 -0
  52. package/dist/cli/commands/start.js.map +1 -1
  53. package/dist/cli/commands/styles-check.d.ts +1 -0
  54. package/dist/cli/commands/styles-check.d.ts.map +1 -1
  55. package/dist/cli/commands/styles-check.js +3 -0
  56. package/dist/cli/commands/styles-check.js.map +1 -1
  57. package/dist/cli/commands/tests.d.ts +1 -0
  58. package/dist/cli/commands/tests.d.ts.map +1 -1
  59. package/dist/cli/commands/tests.js +3 -0
  60. package/dist/cli/commands/tests.js.map +1 -1
  61. package/dist/cli/commands/types.d.ts +5 -0
  62. package/dist/cli/commands/types.d.ts.map +1 -0
  63. package/dist/cli/commands/types.js +3 -0
  64. package/dist/cli/commands/types.js.map +1 -0
  65. package/dist/cli/index.js +24 -37
  66. package/dist/cli/index.js.map +1 -1
  67. package/dist/cli/utils/lerna-exec.d.ts +1 -0
  68. package/dist/cli/utils/lerna-exec.d.ts.map +1 -1
  69. package/dist/cli/utils/lerna-exec.js +3 -0
  70. package/dist/cli/utils/lerna-exec.js.map +1 -1
  71. package/dist/utils/get-packages.d.ts.map +1 -1
  72. package/dist/utils/get-packages.js +29 -12
  73. package/dist/utils/get-packages.js.map +1 -1
  74. package/dist/utils/maybe-create-git-folder.d.ts +10 -0
  75. package/dist/utils/maybe-create-git-folder.d.ts.map +1 -0
  76. package/dist/utils/maybe-create-git-folder.js +25 -0
  77. package/dist/utils/maybe-create-git-folder.js.map +1 -0
  78. package/package.json +12 -12
  79. package/src/cli/commands/__tests__/get-command.test.ts +17 -0
  80. package/src/cli/commands/__tests__/get-user-commands.test.ts +24 -0
  81. package/src/cli/commands/__tests__/init.test.ts +4 -4
  82. package/src/cli/commands/__tests__/install.test.ts +4 -0
  83. package/src/cli/commands/__tests__/mfe-publish.test.ts +11 -0
  84. package/src/cli/commands/build.ts +4 -0
  85. package/src/cli/commands/bundle-package.ts +4 -0
  86. package/src/cli/commands/eslint.ts +4 -0
  87. package/src/cli/commands/get-command.ts +50 -0
  88. package/src/cli/commands/get-user-commands.ts +19 -0
  89. package/src/cli/commands/index.ts +3 -4
  90. package/src/cli/commands/init.ts +7 -3
  91. package/src/cli/commands/install.ts +4 -0
  92. package/src/cli/commands/kendo-ui-license.ts +4 -0
  93. package/src/cli/commands/lint.ts +4 -0
  94. package/src/cli/commands/mfe-publish.ts +31 -12
  95. package/src/cli/commands/prepare-package.ts +4 -0
  96. package/src/cli/commands/start.ts +4 -0
  97. package/src/cli/commands/styles-check.ts +4 -0
  98. package/src/cli/commands/tests.ts +4 -0
  99. package/src/cli/commands/types.ts +4 -0
  100. package/src/cli/index.ts +27 -59
  101. package/src/cli/utils/__tests__/lerna-exec.test.ts +2 -0
  102. package/src/cli/utils/lerna-exec.ts +4 -0
  103. package/src/utils/__tests__/get-packages.test.ts +67 -20
  104. package/src/utils/__tests__/maybe-create-git-folder.test.ts +41 -0
  105. package/src/utils/get-packages.ts +34 -17
  106. package/src/utils/maybe-create-git-folder.ts +18 -0
  107. package/template-react18/packages/application/package.json +1 -1
  108. package/template-react18/packages/feature-a/package.json +1 -1
  109. package/template-react18/packages/feature-b/package.json +1 -1
  110. package/template-react18/packages/feature-c/package.json +1 -1
@@ -11,6 +11,10 @@ interface Args {
11
11
  export class ESLintCommand implements Command {
12
12
  constructor(private args: Args) {}
13
13
 
14
+ description() {
15
+ return undefined;
16
+ }
17
+
14
18
  @logErrors
15
19
  async execute(): Promise<void> {
16
20
  const { _: paths, fix } = this.args;
@@ -0,0 +1,50 @@
1
+ import { CommandName } from '../../utils';
2
+
3
+ import { Build } from './build';
4
+ import { BundlePackage } from './bundle-package';
5
+ import { ESLintCommand } from './eslint';
6
+ import { Init } from './init';
7
+ import { Install } from './install';
8
+ import { KendoUILicense } from './kendo-ui-license';
9
+ import { Lint } from './lint';
10
+ import { MFEPackageClean, MFEPackagePublish, MFEPublish } from './mfe-publish';
11
+ import { PreparePackage } from './prepare-package';
12
+ import { Start } from './start';
13
+ import { StylesCheck } from './styles-check';
14
+ import { Tests } from './tests';
15
+ import { Command } from './types';
16
+
17
+ type Newable<T> = new (...args: any[]) => T;
18
+
19
+ export function getCommand(name: CommandName): Newable<Command> | undefined {
20
+ switch (name) {
21
+ case CommandName.build:
22
+ return Build;
23
+ case CommandName['bundle-package']:
24
+ return BundlePackage;
25
+ case CommandName.eslint:
26
+ return ESLintCommand;
27
+ case CommandName.init:
28
+ return Init;
29
+ case CommandName.install:
30
+ return Install;
31
+ case CommandName['kendo-ui-license']:
32
+ return KendoUILicense;
33
+ case CommandName.lint:
34
+ return Lint;
35
+ case CommandName['mfe-package-clean']:
36
+ return MFEPackageClean;
37
+ case CommandName['mfe-package-publish']:
38
+ return MFEPackagePublish;
39
+ case CommandName['mfe-publish']:
40
+ return MFEPublish;
41
+ case CommandName['prepare-package']:
42
+ return PreparePackage;
43
+ case CommandName.start:
44
+ return Start;
45
+ case CommandName['styles-check']:
46
+ return StylesCheck;
47
+ case CommandName.test:
48
+ return Tests;
49
+ }
50
+ }
@@ -0,0 +1,19 @@
1
+ import { CommandName } from '../../utils';
2
+
3
+ import { getCommand } from './get-command';
4
+
5
+ interface UserCommand {
6
+ name: string;
7
+ description: string;
8
+ }
9
+
10
+ export function getUserCommands() {
11
+ return Object.values(CommandName)
12
+ .sort((a, b) => a.localeCompare(b))
13
+ .map(name => {
14
+ const Command = getCommand(name)!;
15
+ // eslint-disable-next-line @typescript-eslint/naming-convention
16
+ return { name, description: new Command({ _: [] }).description() };
17
+ })
18
+ .filter(({ description }) => !!description) as UserCommand[];
19
+ }
@@ -1,6 +1,8 @@
1
1
  export * from './build';
2
2
  export * from './bundle-package';
3
3
  export * from './eslint';
4
+ export * from './get-command';
5
+ export * from './get-user-commands';
4
6
  export * from './init';
5
7
  export * from './install';
6
8
  export * from './kendo-ui-license';
@@ -10,7 +12,4 @@ export * from './start';
10
12
  export * from './tests';
11
13
  export * from './styles-check';
12
14
  export * from './mfe-publish';
13
-
14
- export interface Command {
15
- execute(): Promise<void>;
16
- }
15
+ export * from './types';
@@ -7,13 +7,17 @@ import { log, logErrors } from '../../utils';
7
7
  import { Command } from './';
8
8
 
9
9
  interface Args {
10
- react18?: boolean;
10
+ react17?: boolean;
11
11
  output?: string;
12
12
  }
13
13
 
14
14
  export class Init implements Command {
15
15
  constructor(private args: Args) {}
16
16
 
17
+ description() {
18
+ return 'generate empty project';
19
+ }
20
+
17
21
  @logErrors
18
22
  async execute() {
19
23
  const destination = path.resolve(this.args.output ?? '.');
@@ -25,11 +29,11 @@ export class Init implements Command {
25
29
 
26
30
  await copyFiles('template', destination);
27
31
 
28
- if (this.args.react18) {
32
+ if (!this.args.react17) {
29
33
  await copyFiles('template-react18', destination);
30
34
  }
31
35
 
32
- log.info(`copied${this.args.react18 ? ' React 18' : ''} template to ${destination}`);
36
+ log.info(`copied${this.args.react17 ? ' React 17' : ''} template to ${destination}`);
33
37
  }
34
38
  }
35
39
 
@@ -4,6 +4,10 @@ import { log, logErrors, getStartupVersion } from '../../utils';
4
4
  import { Command } from '.';
5
5
 
6
6
  export class Install implements Command {
7
+ description() {
8
+ return 'install project dependencies';
9
+ }
10
+
7
11
  @logErrors
8
12
  async execute() {
9
13
  const installCommand = process.env.CI ? 'ci' : 'i';
@@ -30,6 +30,10 @@ const LICENSE = Buffer.from(ENCODED_LICENSE, 'base64').toString('ascii');
30
30
  const LICENSING_PACKAGE = '@progress/kendo-licensing';
31
31
 
32
32
  export class KendoUILicense implements Command {
33
+ description() {
34
+ return 'install KendoReact license key';
35
+ }
36
+
33
37
  async execute() {
34
38
  if (!isModuleInstalled(LICENSING_PACKAGE)) {
35
39
  return;
@@ -32,6 +32,10 @@ export class Lint implements Command {
32
32
  this.paths = [...args._];
33
33
  }
34
34
 
35
+ description() {
36
+ return 'run eslint and stylelint';
37
+ }
38
+
35
39
  async execute() {
36
40
  await this.checkStyles();
37
41
  await this.eslint();
@@ -23,13 +23,14 @@ import { getDefaultBuildVersion } from '../utils/publish';
23
23
  import { Command } from '.';
24
24
 
25
25
  interface ArgsPackagePublish {
26
+ branch?: string;
26
27
  build?: string;
28
+ concurrency?: number;
27
29
  dry?: boolean;
28
30
  force?: boolean;
29
- branch?: string;
30
- tag?: string | false;
31
31
  noTag?: string;
32
32
  registry?: string;
33
+ tag?: string | false;
33
34
  }
34
35
 
35
36
  interface ArgsPackageClean {
@@ -44,6 +45,10 @@ interface Args extends ArgsPackagePublish, ArgsPackageClean {
44
45
  export class MFEPublish implements Command {
45
46
  constructor(private args: Args) {}
46
47
 
48
+ description() {
49
+ return 'publish or unpublish MFE packages';
50
+ }
51
+
47
52
  @logErrors
48
53
  async execute() {
49
54
  let packages = splitPackagesByType(getPackages())[PackageType.Webpack] ?? [];
@@ -67,25 +72,35 @@ export class MFEPublish implements Command {
67
72
 
68
73
  await lernaExec({
69
74
  'cmd': `startup mfe-package-${this.args.clean ? 'clean' : 'publish'}`,
75
+ 'concurrency': this.args.concurrency ?? 1,
70
76
  'scope': packages.map(({ name }) => name),
71
77
  'stream': true,
72
- '--': [
73
- ...[this.args.build ? `--build ${this.args.build}` : undefined],
74
- ...[this.args.branch ? `--branch ${this.args.branch}` : undefined],
75
- ...[this.args.tag ? `--tag ${this.args.tag}` : undefined],
76
- ...[this.args.tag === false ? `--no-tag` : undefined],
77
- ...[this.args.dry ? '--dry' : undefined],
78
- ...[this.args.force ? '--force' : undefined],
79
- ...[this.args.count ? `--count ${this.args.count}` : undefined],
80
- ...[this.args.registry ? `--registry ${this.args.registry}` : undefined],
81
- ].filter(item => !!item) as string[],
78
+ '--': this.getPublishOptions(),
82
79
  });
83
80
  }
81
+
82
+ getPublishOptions() {
83
+ const { build, branch, count, dry, force, registry, tag } = this.args;
84
+ return [
85
+ ...[branch && `--branch ${branch}`],
86
+ ...[build && `--build ${build}`],
87
+ ...[count && `--count ${count}`],
88
+ ...[dry && '--dry'],
89
+ ...[force && '--force'],
90
+ ...[registry && `--registry ${registry}`],
91
+ ...[tag && `--tag ${tag}`],
92
+ ...[tag === false && `--no-tag`],
93
+ ].filter(item => !!item) as string[];
94
+ }
84
95
  }
85
96
 
86
97
  export class MFEPackagePublish implements Command {
87
98
  constructor(private args: ArgsPackagePublish) {}
88
99
 
100
+ description() {
101
+ return undefined;
102
+ }
103
+
89
104
  @logErrors
90
105
  async execute() {
91
106
  if (!isWebComponent()) {
@@ -190,6 +205,10 @@ export class MFEPackagePublish implements Command {
190
205
  export class MFEPackageClean implements Command {
191
206
  constructor(private args: ArgsPackageClean) {}
192
207
 
208
+ description() {
209
+ return undefined;
210
+ }
211
+
193
212
  @logErrors
194
213
  async execute() {
195
214
  if (!isWebComponent()) {
@@ -9,6 +9,10 @@ interface Args {
9
9
  export class PreparePackage implements Command {
10
10
  constructor(private args: Args) {}
11
11
 
12
+ description() {
13
+ return undefined;
14
+ }
15
+
12
16
  @logErrors
13
17
  async execute() {
14
18
  if (!this.args.watch) {
@@ -14,6 +14,10 @@ interface Args {
14
14
  export class Start implements Command {
15
15
  constructor(private args: Args) {}
16
16
 
17
+ description() {
18
+ return 'run project in development mode';
19
+ }
20
+
17
21
  @logErrors
18
22
  async execute() {
19
23
  const packages = splitPackagesByType(
@@ -121,6 +121,10 @@ function checkStylesWebComponent(files: FileInfo[]) {
121
121
  }
122
122
 
123
123
  export class StylesCheck implements Command {
124
+ description() {
125
+ return undefined;
126
+ }
127
+
124
128
  // eslint-disable-next-line @typescript-eslint/require-await
125
129
  async execute() {
126
130
  if (isLegacy()) {
@@ -9,6 +9,10 @@ import { Command } from '.';
9
9
  export class Tests implements Command {
10
10
  constructor(private args: Config.Argv) {}
11
11
 
12
+ description() {
13
+ return 'run tests';
14
+ }
15
+
12
16
  @logErrors
13
17
  async execute() {
14
18
  const jestConfig = getJestConfigCLI(this.args);
@@ -0,0 +1,4 @@
1
+ export interface Command {
2
+ execute(): Promise<void>;
3
+ description(): string | undefined;
4
+ }
package/src/cli/index.ts CHANGED
@@ -1,69 +1,23 @@
1
1
  import execa from 'execa';
2
2
  import { argv, Arguments } from 'yargs';
3
-
4
- import { CommandName, log } from '../utils';
5
- import {
6
- Build,
7
- BundlePackage,
8
- Command,
9
- ESLintCommand,
10
- Init,
11
- Install,
12
- KendoUILicense,
13
- Lint,
14
- MFEPackageClean,
15
- MFEPackagePublish,
16
- MFEPublish,
17
- PreparePackage,
18
- Start,
19
- StylesCheck,
20
- Tests,
21
- } from './commands';
3
+ import { CommandName, getStartupVersion, log } from '../utils';
4
+ import { getCommand, getUserCommands } from './commands';
22
5
  import { setNodeOptions } from './utils';
23
6
 
24
- interface Newable<T> {
25
- new (...args: any[]): T;
26
- }
27
-
28
- function getCommand(name: CommandName): Newable<Command> {
29
- switch (name) {
30
- case CommandName.build:
31
- return Build;
32
- case CommandName['bundle-package']:
33
- return BundlePackage;
34
- case CommandName.eslint:
35
- return ESLintCommand;
36
- case CommandName.init:
37
- return Init;
38
- case CommandName.install:
39
- return Install;
40
- case CommandName['kendo-ui-license']:
41
- return KendoUILicense;
42
- case CommandName.lint:
43
- return Lint;
44
- case CommandName['mfe-package-clean']:
45
- return MFEPackageClean;
46
- case CommandName['mfe-package-publish']:
47
- return MFEPackagePublish;
48
- case CommandName['mfe-publish']:
49
- return MFEPublish;
50
- case CommandName['prepare-package']:
51
- return PreparePackage;
52
- case CommandName.start:
53
- return Start;
54
- case CommandName['styles-check']:
55
- return StylesCheck;
56
- case CommandName.test:
57
- return Tests;
58
- default:
59
- log.error(`${name}: command not found!`);
60
- process.exit(127);
61
- }
7
+ const argvSync = argv as Arguments;
8
+ const name = argvSync._[0]?.toString() as CommandName;
9
+ if (!name) {
10
+ log.info(`startup cli v${getStartupVersion()}`);
11
+ usage();
12
+ process.exit(0);
62
13
  }
63
14
 
64
- const argvSync = argv as Arguments;
65
- const name = argvSync._[0].toString() as CommandName;
66
15
  const Command = getCommand(name);
16
+ if (!Command) {
17
+ log.error(`Unknown command: "${name}"`);
18
+ usage();
19
+ process.exit(127);
20
+ }
67
21
 
68
22
  if (setNodeOptions(name)) {
69
23
  // Run command in child process with amended NODE_OPTIONS
@@ -77,3 +31,17 @@ if (setNodeOptions(name)) {
77
31
  process.exit(1);
78
32
  });
79
33
  }
34
+
35
+ function usage() {
36
+ write('\nUsage:\n');
37
+
38
+ const commands = getUserCommands().filter(({ name }) => !!name);
39
+ const maxNameLength = commands.reduce((result, { name }) => Math.max(result, name.length), 0);
40
+ commands.forEach(({ name, description }) => {
41
+ write(`startup ${name.padEnd(maxNameLength, ' ')} ${description}`);
42
+ });
43
+ }
44
+
45
+ function write(text: string) {
46
+ return console.info(text); // eslint-disable-line no-console
47
+ }
@@ -31,6 +31,8 @@ describe(`${lernaExec.name}`, () => {
31
31
  { arg: { parallel: true }, option: '--parallel' },
32
32
  { arg: { stream: true }, option: '--stream' },
33
33
  { arg: { bail: false }, option: '--no-bail' },
34
+ { arg: { concurrency: 1 }, option: '--concurrency=1' },
35
+ { arg: { concurrency: 0 }, option: '--concurrency=0' },
34
36
  ] as { arg: Partial<typeof args>; option: string }[])('with $arg', ({ arg, option }) => {
35
37
  beforeEach(() => Object.assign(args, arg));
36
38
 
@@ -4,6 +4,7 @@ import { log } from '../../utils';
4
4
  interface Args {
5
5
  'bail'?: boolean;
6
6
  'cmd': string;
7
+ 'concurrency'?: number;
7
8
  'scope'?: string[];
8
9
  'stream'?: boolean;
9
10
  'parallel'?: boolean;
@@ -33,5 +34,8 @@ function getOptions(args: Args) {
33
34
  if (args.stream) {
34
35
  result.push('--stream');
35
36
  }
37
+ if (typeof args.concurrency === 'number') {
38
+ result.push(`--concurrency=${args.concurrency}`);
39
+ }
36
40
  return result;
37
41
  }
@@ -1,5 +1,7 @@
1
1
  import execa from 'execa';
2
2
  import { isBundle, isLegacy } from '../get-configuration';
3
+ import { log } from '../log';
4
+ import { maybeCreateGitFolder } from '../maybe-create-git-folder';
3
5
 
4
6
  import {
5
7
  Package,
@@ -11,6 +13,7 @@ import {
11
13
 
12
14
  jest.mock('execa', () => ({ sync: jest.fn() }));
13
15
  jest.mock('../get-configuration');
16
+ jest.mock('../maybe-create-git-folder');
14
17
 
15
18
  describe('[startup] Utils', () => {
16
19
  const packages: Pick<Package, 'name' | 'location' | 'type'>[] = [
@@ -19,6 +22,15 @@ describe('[startup] Utils', () => {
19
22
  { name: 'baz', location: 'packages/baz', type: PackageType.Webpack },
20
23
  ];
21
24
 
25
+ beforeEach(() => jest.clearAllMocks());
26
+
27
+ function expectExecaToBeCalledWith(command: string, options?: execa.SyncOptions) {
28
+ const [file, ...args] = command.split(' ');
29
+ const { calls } = jest.mocked(execa.sync).mock;
30
+ const expected: any[] = [file, args, ...(options ? [options] : [])];
31
+ expect(calls).toContainEqual(expect.arrayContaining(expected));
32
+ }
33
+
22
34
  describe(`${getPackages.name}`, () => {
23
35
  let options: Parameters<typeof getPackages>[0] | undefined;
24
36
  let dependencies: Record<string, string[]> | undefined;
@@ -40,6 +52,16 @@ describe('[startup] Utils', () => {
40
52
  return result;
41
53
  }
42
54
 
55
+ function resolvePackages(args: string[]) {
56
+ if (args.includes('--graph')) {
57
+ return packageGraph();
58
+ }
59
+ if (args.includes('--scope')) {
60
+ return scopedPackages().map(({ type, ...pkg }) => pkg);
61
+ }
62
+ return packages.map(({ type, ...pkg }) => pkg);
63
+ }
64
+
43
65
  beforeEach(() => {
44
66
  options = undefined;
45
67
  dependencies = undefined;
@@ -52,15 +74,7 @@ describe('[startup] Utils', () => {
52
74
  );
53
75
  // @ts-expect-error because implementation doesn't match all exec.sync signatures
54
76
  jest.mocked(execa.sync).mockImplementation((_: string, args: string[]): any => {
55
- return {
56
- stdout: JSON.stringify(
57
- args.includes('--graph')
58
- ? packageGraph()
59
- : args.includes('--scope')
60
- ? scopedPackages().map(({ type, ...pkg }) => pkg)
61
- : packages.map(({ type, ...pkg }) => pkg)
62
- ),
63
- };
77
+ return { stdout: JSON.stringify(resolvePackages(args)) };
64
78
  });
65
79
  });
66
80
 
@@ -68,7 +82,34 @@ describe('[startup] Utils', () => {
68
82
 
69
83
  test('returns lerna packages with type metadata', () => {
70
84
  expect(subject()).toEqual(packages);
71
- expect(execa.sync).toHaveBeenCalledWith('npx', 'lerna la --json'.split(' '));
85
+ expectExecaToBeCalledWith('npx lerna la --json');
86
+ });
87
+
88
+ test('conditionally creates .git folder', () => {
89
+ subject();
90
+
91
+ expect(maybeCreateGitFolder).toHaveBeenCalled();
92
+ });
93
+
94
+ describe('when running on Windows', () => {
95
+ const originalPlatform = process.platform;
96
+ beforeEach(() => {
97
+ Object.defineProperty(process, 'platform', { value: 'win32' });
98
+ });
99
+
100
+ afterEach(() => {
101
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
102
+ });
103
+
104
+ test('sets NX_CACHE_PROJECT_GRAPH=false', () => {
105
+ subject();
106
+
107
+ expectExecaToBeCalledWith('npx lerna la --json', {
108
+ extendEnv: true,
109
+ // eslint-disable-next-line @typescript-eslint/naming-convention
110
+ env: { NX_CACHE_PROJECT_GRAPH: 'false' },
111
+ });
112
+ });
72
113
  });
73
114
 
74
115
  describe.each(['scope', 'ignore'])('with "%s"', option => {
@@ -77,10 +118,7 @@ describe('[startup] Utils', () => {
77
118
  test(`passes "${option}" option to lerna`, () => {
78
119
  subject();
79
120
 
80
- expect(execa.sync).toHaveBeenCalledWith(
81
- 'npx',
82
- `lerna la --${option} foo --json`.split(' ')
83
- );
121
+ expectExecaToBeCalledWith(`npx lerna la --${option} foo --json`);
84
122
  });
85
123
  });
86
124
 
@@ -101,6 +139,18 @@ describe('[startup] Utils', () => {
101
139
  });
102
140
  });
103
141
  });
142
+
143
+ describe('when execa returns unexpected output', () => {
144
+ beforeEach(() =>
145
+ jest.mocked(execa.sync).mockReturnValue({ stdout: Buffer.from('foo') } as any)
146
+ );
147
+
148
+ test('throws an error and logs the output', () => {
149
+ const logSpy = jest.spyOn(log, 'error');
150
+ expect(() => subject()).toThrow();
151
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('\nfoo'));
152
+ });
153
+ });
104
154
  });
105
155
 
106
156
  describe(`${getPackagesGraph.name}`, () => {
@@ -118,7 +168,7 @@ describe('[startup] Utils', () => {
118
168
 
119
169
  test('returns lerna package graph', () => {
120
170
  expect(subject()).toEqual(packageGraph);
121
- expect(execa.sync).toHaveBeenCalledWith('npx', 'lerna la --graph'.split(' '));
171
+ expectExecaToBeCalledWith('npx lerna la --graph');
122
172
  });
123
173
 
124
174
  describe.each(['scope', 'ignore'])('with "%s"', option => {
@@ -127,16 +177,13 @@ describe('[startup] Utils', () => {
127
177
  test(`passes "${option}" option to lerna`, () => {
128
178
  subject();
129
179
 
130
- expect(execa.sync).toHaveBeenCalledWith(
131
- 'npx',
132
- `lerna la --${option} foo --graph`.split(' ')
133
- );
180
+ expectExecaToBeCalledWith(`npx lerna la --${option} foo --graph`);
134
181
  });
135
182
  });
136
183
  });
137
184
 
138
185
  describe(`${splitPackagesByType.name}`, () => {
139
- const subject = (packages: Package[]) => splitPackagesByType(packages as Package[]);
186
+ const subject = (packages: Package[]) => splitPackagesByType(packages);
140
187
 
141
188
  test('groups packages by type', () => {
142
189
  expect(subject(packages as Package[])).toEqual(
@@ -0,0 +1,41 @@
1
+ import { fs, vol } from 'memfs';
2
+
3
+ import { maybeCreateGitFolder } from '../maybe-create-git-folder';
4
+
5
+ jest.mock('fs', () => fs);
6
+
7
+ describe(`[startup] Utils`, () => {
8
+ describe(`${maybeCreateGitFolder.name}`, () => {
9
+ const mkdirSpy = jest.spyOn(fs, 'mkdirSync').mockImplementation(jest.fn());
10
+
11
+ beforeEach(() => vol.fromJSON({}));
12
+
13
+ afterEach(() => vol.reset);
14
+
15
+ const subject = () => maybeCreateGitFolder();
16
+
17
+ describe('when running on Windows', () => {
18
+ beforeEach(() => {
19
+ Object.defineProperty(process, 'platform', { value: 'win32' });
20
+ });
21
+
22
+ test('creates .git folder', () => {
23
+ subject();
24
+
25
+ expect(mkdirSpy).toHaveBeenCalledWith('.git');
26
+ });
27
+ });
28
+
29
+ describe('when not running on Windows', () => {
30
+ beforeEach(() => {
31
+ Object.defineProperty(process, 'platform', { value: 'linux' });
32
+ });
33
+
34
+ test('does not create .git folder', () => {
35
+ subject();
36
+
37
+ expect(mkdirSpy).not.toHaveBeenCalledWith();
38
+ });
39
+ });
40
+ });
41
+ });