@servicetitan/startup 27.4.0 → 28.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 (141) hide show
  1. package/dist/cli/commands/get-command.d.ts.map +1 -1
  2. package/dist/cli/commands/get-command.js +4 -2
  3. package/dist/cli/commands/get-command.js.map +1 -1
  4. package/dist/cli/commands/init.d.ts +0 -1
  5. package/dist/cli/commands/init.d.ts.map +1 -1
  6. package/dist/cli/commands/init.js +39 -9
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/mfe-package-clean.d.ts +14 -0
  9. package/dist/cli/commands/mfe-package-clean.d.ts.map +1 -0
  10. package/dist/cli/commands/mfe-package-clean.js +124 -0
  11. package/dist/cli/commands/mfe-package-clean.js.map +1 -0
  12. package/dist/cli/commands/mfe-package-publish.d.ts +20 -0
  13. package/dist/cli/commands/mfe-package-publish.d.ts.map +1 -0
  14. package/dist/cli/commands/mfe-package-publish.js +153 -0
  15. package/dist/cli/commands/mfe-package-publish.js.map +1 -0
  16. package/dist/cli/commands/mfe-publish.d.ts +6 -30
  17. package/dist/cli/commands/mfe-publish.d.ts.map +1 -1
  18. package/dist/cli/commands/mfe-publish.js +14 -226
  19. package/dist/cli/commands/mfe-publish.js.map +1 -1
  20. package/dist/cli/utils/assets-copy.d.ts.map +1 -1
  21. package/dist/cli/utils/assets-copy.js +3 -3
  22. package/dist/cli/utils/assets-copy.js.map +1 -1
  23. package/dist/cli/utils/cli-os.d.ts +9 -2
  24. package/dist/cli/utils/cli-os.d.ts.map +1 -1
  25. package/dist/cli/utils/cli-os.js +16 -8
  26. package/dist/cli/utils/cli-os.js.map +1 -1
  27. package/dist/cli/utils/styles-copy.d.ts.map +1 -1
  28. package/dist/cli/utils/styles-copy.js +3 -3
  29. package/dist/cli/utils/styles-copy.js.map +1 -1
  30. package/dist/utils/get-branch-configs.d.ts +3 -0
  31. package/dist/utils/get-branch-configs.d.ts.map +1 -0
  32. package/dist/utils/get-branch-configs.js +18 -0
  33. package/dist/utils/get-branch-configs.js.map +1 -0
  34. package/dist/utils/get-configuration.d.ts +13 -3
  35. package/dist/utils/get-configuration.d.ts.map +1 -1
  36. package/dist/utils/get-configuration.js +28 -5
  37. package/dist/utils/get-configuration.js.map +1 -1
  38. package/dist/utils/get-folders.js +2 -2
  39. package/dist/utils/get-folders.js.map +1 -1
  40. package/dist/utils/get-jest-config.d.ts.map +1 -1
  41. package/dist/utils/get-jest-config.js +2 -10
  42. package/dist/utils/get-jest-config.js.map +1 -1
  43. package/dist/webpack/configs/dev-server-config.d.ts.map +1 -1
  44. package/dist/webpack/configs/dev-server-config.js +1 -1
  45. package/dist/webpack/configs/dev-server-config.js.map +1 -1
  46. package/dist/webpack/configs/plugins/ignore-plugin/is-optional-anvil-peer-dependency.js +1 -1
  47. package/dist/webpack/configs/plugins/ignore-plugin/is-optional-anvil-peer-dependency.js.map +1 -1
  48. package/dist/webpack/configs/plugins/index.d.ts +1 -0
  49. package/dist/webpack/configs/plugins/index.d.ts.map +1 -1
  50. package/dist/webpack/configs/plugins/index.js +1 -0
  51. package/dist/webpack/configs/plugins/index.js.map +1 -1
  52. package/dist/webpack/configs/plugins/virtual-modules-plugin.d.ts.map +1 -1
  53. package/dist/webpack/configs/plugins/virtual-modules-plugin.js +15 -5
  54. package/dist/webpack/configs/plugins/virtual-modules-plugin.js.map +1 -1
  55. package/dist/webpack/configs/plugins/watch-run-plugin.d.ts +8 -0
  56. package/dist/webpack/configs/plugins/watch-run-plugin.d.ts.map +1 -0
  57. package/dist/webpack/configs/plugins/watch-run-plugin.js +26 -0
  58. package/dist/webpack/configs/plugins/watch-run-plugin.js.map +1 -0
  59. package/dist/webpack/configs/plugins-config.d.ts.map +1 -1
  60. package/dist/webpack/configs/plugins-config.js +1 -0
  61. package/dist/webpack/configs/plugins-config.js.map +1 -1
  62. package/dist/webpack/configs/types.d.ts +16 -0
  63. package/dist/webpack/configs/types.d.ts.map +1 -1
  64. package/dist/webpack/configs/utils/generate-metadata.d.ts.map +1 -1
  65. package/dist/webpack/configs/utils/generate-metadata.js +3 -3
  66. package/dist/webpack/configs/utils/generate-metadata.js.map +1 -1
  67. package/jest/jest-preset.js +9 -0
  68. package/package.json +18 -23
  69. package/src/cli/commands/__tests__/init.test.ts +108 -28
  70. package/src/cli/commands/__tests__/mfe-package-clean.test.ts +45 -6
  71. package/src/cli/commands/__tests__/mfe-package-publish.test.ts +77 -7
  72. package/src/cli/commands/__tests__/mfe-publish.test.ts +19 -1
  73. package/src/cli/commands/__tests__/tests.test.ts +4 -0
  74. package/src/cli/commands/get-command.ts +3 -1
  75. package/src/cli/commands/init.ts +40 -10
  76. package/src/cli/commands/mfe-package-clean.ts +143 -0
  77. package/src/cli/commands/mfe-package-publish.ts +189 -0
  78. package/src/cli/commands/mfe-publish.ts +18 -298
  79. package/src/cli/utils/__tests__/assets-copy.test.ts +3 -7
  80. package/src/cli/utils/__tests__/cli-os.test.ts +41 -6
  81. package/src/cli/utils/__tests__/eslint.test.ts +4 -0
  82. package/src/cli/utils/__tests__/styles-copy.test.ts +3 -7
  83. package/src/cli/utils/assets-copy.ts +3 -3
  84. package/src/cli/utils/cli-os.ts +20 -8
  85. package/src/cli/utils/styles-copy.ts +3 -3
  86. package/src/utils/__tests__/get-branch-configs.test.ts +36 -0
  87. package/src/utils/__tests__/get-configuration.test.ts +65 -0
  88. package/src/utils/__tests__/get-jest-config.test.ts +1 -7
  89. package/src/utils/__tests__/load-shared-dependencies.test.ts +82 -88
  90. package/src/utils/get-branch-configs.ts +17 -0
  91. package/src/utils/get-configuration.ts +45 -7
  92. package/src/utils/get-folders.ts +1 -1
  93. package/src/utils/get-jest-config.ts +2 -10
  94. package/src/webpack/__tests__/create-webpack-config-shared-dependencies.test.ts +0 -1
  95. package/src/webpack/__tests__/create-webpack-config-web-component.test.ts +47 -13
  96. package/src/webpack/__tests__/create-webpack-config.test.ts +3 -2
  97. package/src/webpack/configs/dev-server-config.ts +2 -1
  98. package/src/webpack/configs/plugins/ignore-plugin/is-optional-anvil-peer-dependency.ts +1 -1
  99. package/src/webpack/configs/plugins/index.ts +1 -0
  100. package/src/webpack/configs/plugins/virtual-modules-plugin.ts +17 -5
  101. package/src/webpack/configs/plugins/watch-run-plugin.ts +23 -0
  102. package/src/webpack/configs/plugins-config.ts +2 -0
  103. package/src/webpack/configs/types.ts +19 -0
  104. package/src/webpack/configs/utils/generate-metadata.ts +5 -5
  105. package/tsconfig/base.json +1 -1
  106. package/template/.eslintrc.json +0 -3
  107. package/template/.gitignore +0 -21
  108. package/template/.npmrc +0 -3
  109. package/template/.prettierrc +0 -9
  110. package/template/.stylelintignore +0 -1
  111. package/template/.stylelintrc.json +0 -3
  112. package/template/.vscode/extensions.json +0 -18
  113. package/template/.vscode/settings.json +0 -4
  114. package/template/lerna.json +0 -4
  115. package/template/package.json +0 -32
  116. package/template/packages/application/package.json +0 -35
  117. package/template/packages/application/src/__tests__/app.test.tsx +0 -33
  118. package/template/packages/application/src/app.css +0 -3
  119. package/template/packages/application/src/app.tsx +0 -45
  120. package/template/packages/application/src/design-system.css +0 -3
  121. package/template/packages/application/src/index.tsx +0 -8
  122. package/template/packages/application/src/main-page.tsx +0 -5
  123. package/template/packages/application/src/second-page.tsx +0 -5
  124. package/template/packages/application/tsconfig.json +0 -13
  125. package/template/packages/feature-a/package.json +0 -19
  126. package/template/packages/feature-a/src/index.ts +0 -0
  127. package/template/packages/feature-a/tsconfig.json +0 -9
  128. package/template/packages/feature-b/package.json +0 -19
  129. package/template/packages/feature-b/src/index.ts +0 -0
  130. package/template/packages/feature-b/tsconfig.json +0 -9
  131. package/template/packages/feature-c/package.json +0 -19
  132. package/template/packages/feature-c/src/index.ts +0 -0
  133. package/template/packages/feature-c/tsconfig.json +0 -9
  134. package/template/setupTests.ts +0 -27
  135. package/template/tsconfig.test.json +0 -5
  136. package/template-react18/packages/application/package.json +0 -35
  137. package/template-react18/packages/application/src/index.tsx +0 -9
  138. package/template-react18/packages/feature-a/package.json +0 -19
  139. package/template-react18/packages/feature-b/package.json +0 -19
  140. package/template-react18/packages/feature-c/package.json +0 -19
  141. package/tsconfig.json +0 -13
@@ -1,67 +1,118 @@
1
- import cpx from 'cpx2';
2
- import path from 'path';
3
1
  import { fs, vol } from 'memfs';
2
+ import { mkdirSync, rmSync } from 'fs';
3
+ import path from 'path';
4
4
 
5
5
  import { Init } from '../init';
6
+ import { runCommand, runCommandOutput } from '../../utils/cli-os';
6
7
 
7
8
  jest.mock('fs', () => fs);
8
-
9
- jest.mock('cpx2', () => ({
10
- copy: jest.fn().mockImplementation((...args: any[]) => {
11
- const callback = args[args.length - 1];
12
- callback(null);
13
- }),
14
- }));
15
-
9
+ jest.mock('../../utils//cli-os', () => ({ runCommand: jest.fn(), runCommandOutput: jest.fn() }));
16
10
  jest.mock('../../../utils', () => ({
17
11
  log: { info: jest.fn() }, // suppress log output
18
12
  }));
19
13
 
14
+ const webUrl = 'https://github.com/servicetitan/frontend-example.git';
15
+ const sshUrl = 'git@github.com:servicetitan/frontend-example.git';
16
+
20
17
  describe(`[startup] ${Init.name}`, () => {
21
18
  let args: ConstructorParameters<typeof Init>[0];
22
19
 
23
20
  beforeEach(() => {
24
21
  args = {};
25
22
  vol.reset();
23
+ jest.clearAllMocks();
24
+ jest.mocked(runCommand).mockImplementation(jest.fn());
25
+ jest.mocked(runCommandOutput).mockImplementation(jest.fn());
26
+ jest.spyOn(fs, 'mkdirSync').mockImplementation(jest.fn());
27
+ jest.spyOn(fs, 'rmSync').mockImplementation(jest.fn());
26
28
  });
27
29
 
28
30
  const subject = async () => new Init(args).execute();
29
31
 
30
- function itCopies(...names: string[]) {
31
- test.each(names)(`copies %s to current directory`, async (name: string) => {
32
+ test(`clones ${webUrl} to current directory`, async () => {
33
+ const cwd = path.resolve('.');
34
+ await subject();
35
+
36
+ expect(runCommand).toHaveBeenCalledWith(`git clone -q ${webUrl} ${cwd}`, {
37
+ quiet: true,
38
+ });
39
+ expect(rmSync).toHaveBeenCalledWith(path.join(cwd, '.git'), {
40
+ recursive: true,
41
+ force: true,
42
+ });
43
+ expect(rmSync).toHaveBeenCalledWith(path.join(cwd, '.github', 'CODEOWNERS'));
44
+ expect(rmSync).toHaveBeenCalledWith(path.join(cwd, 'package-lock.json'));
45
+ });
46
+
47
+ describe(`when cloning ${webUrl} fails`, () => {
48
+ beforeEach(() => jest.mocked(runCommand).mockRejectedValueOnce('Nope!'));
49
+
50
+ test(`clones ${sshUrl} to current directory`, async () => {
32
51
  await subject();
33
52
 
34
- expect(cpx.copy).toHaveBeenCalledWith(
35
- expect.stringContaining(path.join(name, '**', '{.*,*,.*/*}')),
36
- process.cwd(),
53
+ expect(runCommand).toHaveBeenCalledWith(
54
+ `git clone -q ${sshUrl} ${path.resolve('.')}`,
37
55
  expect.anything()
38
56
  );
39
57
  });
40
- }
41
58
 
42
- itCopies('template', 'template-react18');
59
+ describe(`when cloning ${sshUrl} also fails`, () => {
60
+ beforeEach(() => {
61
+ jest.mocked(runCommand).mockRejectedValue('Nope!');
62
+ });
43
63
 
44
- describe('when instructed to create React 17 template', () => {
45
- beforeEach(() => (args.react17 = true));
64
+ test(`checks if ${webUrl} is reachable`, async () => {
65
+ await subject();
46
66
 
47
- itCopies('template');
67
+ expect(runCommandOutput).toHaveBeenCalledWith(`git ls-remote -qt ${webUrl}`, {
68
+ quiet: true,
69
+ });
70
+ });
71
+
72
+ describe(`when ${webUrl} is not reachable`, () => {
73
+ beforeEach(() => {
74
+ jest.mocked(runCommandOutput).mockImplementationOnce(() => {
75
+ throw new Error('Oops!');
76
+ });
77
+ });
78
+
79
+ test(`checks if ${sshUrl} is reachable`, async () => {
80
+ await subject();
81
+
82
+ expect(runCommandOutput).toHaveBeenCalledWith(`git ls-remote -qt ${sshUrl}`, {
83
+ quiet: true,
84
+ });
85
+ });
86
+
87
+ describe(`when ${sshUrl} is also unreachable`, () => {
88
+ beforeEach(() =>
89
+ jest.mocked(runCommandOutput).mockImplementation(() => {
90
+ throw new Error('Oops');
91
+ })
92
+ );
93
+
94
+ test('raises error', async () => {
95
+ await expect(subject()).rejects.toThrow(
96
+ /could not read servicetitan\/frontend-example repository/
97
+ );
98
+ });
99
+ });
100
+ });
101
+ });
48
102
  });
49
103
 
50
104
  describe('with an output location', () => {
51
105
  beforeEach(() => (args.output = 'foo/bar'));
52
106
 
53
- test('copies template to output location', async () => {
54
- const mkdirSpy = jest.spyOn(fs, 'mkdirSync');
107
+ test(`clones ${webUrl} to output location`, async () => {
55
108
  const destination = path.resolve(args.output!);
56
109
 
57
110
  await subject();
58
111
 
59
- expect(mkdirSpy).toHaveBeenCalledWith(destination, { recursive: true });
60
- expect(cpx.copy).toHaveBeenCalledWith(
61
- expect.any(String),
62
- destination,
63
- expect.anything()
64
- );
112
+ expect(mkdirSync).toHaveBeenCalledWith(destination, { recursive: true });
113
+ expect(runCommand).toHaveBeenCalledWith(`git clone -q ${webUrl} ${destination}`, {
114
+ quiet: true,
115
+ });
65
116
  });
66
117
 
67
118
  describe('when output location is a file', () => {
@@ -71,5 +122,34 @@ describe(`[startup] ${Init.name}`, () => {
71
122
  await expect(subject()).rejects.toThrow(/is not a directory/);
72
123
  });
73
124
  });
125
+
126
+ describe('when output location is not empty', () => {
127
+ beforeEach(() => vol.fromNestedJSON({ [args.output!]: { baz: '' } }));
128
+
129
+ test('raises error', async () => {
130
+ await expect(subject()).rejects.toThrow(/is not an empty directory/);
131
+ });
132
+ });
133
+ });
134
+
135
+ describe('when running in CI environment with GITHUB_TOKEN', () => {
136
+ const originalEnv = process.env;
137
+ const token = 'foo-bar';
138
+
139
+ beforeEach(() => {
140
+ process.env.CI = 'true';
141
+ process.env.GITHUB_TOKEN = token;
142
+ });
143
+ afterEach(() => (process.env = originalEnv));
144
+
145
+ test(`adds token to ${webUrl}`, async () => {
146
+ await subject();
147
+
148
+ const urlWithToken = webUrl.replace('github.com', `oauth2:${token}@github.com`);
149
+ expect(runCommand).toHaveBeenCalledWith(
150
+ `git clone -q ${urlWithToken} ${path.resolve('.')}`,
151
+ expect.anything()
152
+ );
153
+ });
74
154
  });
75
155
  });
@@ -1,8 +1,9 @@
1
1
  import { fs, vol } from 'memfs';
2
2
  import { isWebComponent, log } from '../../../utils';
3
+ import { gitGetBranch } from '../../utils/cli-git';
3
4
  import { npmGetPackageVersionDates, npmUnpublish } from '../../utils/cli-npm';
4
5
 
5
- import { MFEPackageClean } from '../mfe-publish';
6
+ import { MFEPackageClean } from '../mfe-package-clean';
6
7
 
7
8
  jest.mock('fs', () => fs);
8
9
  jest.mock('../../../utils', () => ({
@@ -14,6 +15,9 @@ jest.mock('../../utils/cli-npm', () => ({
14
15
  npmGetPackageVersionDates: jest.fn(),
15
16
  npmUnpublish: jest.fn(),
16
17
  }));
18
+ jest.mock('../../utils/cli-git', () => ({
19
+ gitGetBranch: jest.fn(),
20
+ }));
17
21
 
18
22
  const DEFAULT_VERSIONS_TO_KEEP = 5;
19
23
 
@@ -23,18 +27,23 @@ describe(`[startup] ${MFEPackageClean.name}`, () => {
23
27
  let args: ConstructorParameters<typeof MFEPackageClean>[0];
24
28
  let versions: Record<string, Date>;
25
29
 
26
- beforeEach(() => {
27
- args = {};
28
- versions = Object.fromEntries(
29
- Array.from({ length: 10 }).map((_, index) => [
30
- `0.0.0-master.${(0xff000000 + index).toString(16)}`,
30
+ function getBranchVersions(branchName: string, length: number) {
31
+ return Object.fromEntries(
32
+ Array.from({ length }).map((_, index) => [
33
+ `0.0.0-${branchName}.${(0xff000000 + index).toString(16)}`,
31
34
  dayAgo(index + 1), // tests assume versions are in reverse chronological order
32
35
  ])
33
36
  );
37
+ }
38
+
39
+ beforeEach(() => {
40
+ args = {};
41
+ versions = getBranchVersions('master', 10);
34
42
 
35
43
  jest.resetAllMocks();
36
44
  jest.mocked(isWebComponent).mockReturnValue(true);
37
45
  jest.mocked(npmGetPackageVersionDates).mockImplementation(() => Object.entries(versions));
46
+ jest.mocked(gitGetBranch).mockImplementation(() => 'current');
38
47
  vol.fromJSON({ 'package.json': JSON.stringify({ name: packageName }) });
39
48
  });
40
49
 
@@ -74,6 +83,36 @@ describe(`[startup] ${MFEPackageClean.name}`, () => {
74
83
  itLogsAndUnpublishedOldestPackages({ branch });
75
84
  });
76
85
 
86
+ describe.each(['qa', true])('when selected branch is %s', (branch: string | true) => {
87
+ let branchedVersions: typeof versions = {};
88
+ const branchName = branch === true ? 'current' : branch;
89
+
90
+ beforeEach(() => {
91
+ args.branch = branch;
92
+
93
+ branchedVersions = getBranchVersions(branchName, 7);
94
+
95
+ versions = {
96
+ ...versions,
97
+ ...branchedVersions,
98
+ };
99
+ });
100
+
101
+ test(`logs and unpublishes oldest packages for ${branchName} branch`, async () => {
102
+ const oldest = Object.entries(branchedVersions).slice(DEFAULT_VERSIONS_TO_KEEP);
103
+
104
+ await subject();
105
+
106
+ expect(log.info).toHaveBeenCalledWith(
107
+ 'found versions for unpublish:',
108
+ JSON.stringify({ [branchName]: oldest }, null, 4)
109
+ );
110
+ oldest.forEach(([version]) => {
111
+ expect(npmUnpublish).toHaveBeenCalledWith(registry, packageName, version);
112
+ });
113
+ });
114
+ });
115
+
77
116
  describe('with master versions generated by nerdbank versioning', () => {
78
117
  beforeEach(() => {
79
118
  versions = transform(versions, (v: string, index: number) =>
@@ -1,5 +1,6 @@
1
1
  import { fs, vol } from 'memfs';
2
- import { isWebComponent, log } from '../../../utils';
2
+ import path from 'path';
3
+ import { isWebComponent, log, readJson } from '../../../utils';
3
4
  import { gitGetBranch, gitGetCommitHash } from '../../utils/cli-git';
4
5
  import {
5
6
  npmGetPackageVersions,
@@ -8,7 +9,7 @@ import {
8
9
  npmPublishDry,
9
10
  npmTagVersion,
10
11
  } from '../../utils/cli-npm';
11
- import { MFEPackagePublish } from '../mfe-publish';
12
+ import { MFEPackagePublish } from '../mfe-package-publish';
12
13
 
13
14
  jest.mock('fs', () => fs);
14
15
  jest.mock('../../../utils', () => ({
@@ -44,7 +45,30 @@ describe(`[startup] ${MFEPackagePublish.name}`, () => {
44
45
  jest.mocked(gitGetBranch).mockReturnValue(branch);
45
46
  jest.mocked(gitGetCommitHash).mockReturnValue(commitHash);
46
47
  jest.mocked(npmGetPackageVersions).mockReturnValue([]);
47
- vol.fromJSON({ 'package.json': JSON.stringify({ name: packageName, files: [] }) });
48
+ vol.fromNestedJSON({
49
+ 'package.json': JSON.stringify({ name: packageName, files: [] }),
50
+ 'tsconfig.json': JSON.stringify({
51
+ compilerOptions: { outDir: 'dist', rootDir: 'src' },
52
+ }),
53
+ 'dist': {
54
+ 'metadata.json': JSON.stringify({
55
+ entrypoints: {
56
+ full: { css: ['main.bundle.css'], js: ['main.bundle.js'] },
57
+ light: { css: ['main.bundle.css'], js: ['main.bundle.js'] },
58
+ },
59
+ }),
60
+ 'bundle': {
61
+ full: {
62
+ 'main.bundle.css': '',
63
+ 'main.bundle.js': '',
64
+ },
65
+ light: {
66
+ 'main.bundle.css': '',
67
+ 'main.bundle.js': '',
68
+ },
69
+ },
70
+ },
71
+ });
48
72
  });
49
73
 
50
74
  afterEach(() => vol.reset());
@@ -239,17 +263,63 @@ describe(`[startup] ${MFEPackagePublish.name}`, () => {
239
263
  });
240
264
  });
241
265
 
242
- describe('when package is not web component', () => {
243
- beforeEach(() => jest.mocked(isWebComponent).mockReturnValue(false));
244
-
266
+ function itThrowsError(message: RegExp) {
245
267
  test('throws error', async () => {
246
268
  jest.spyOn(process.stdout, 'write').mockImplementation(jest.fn()); // suppress error output
247
269
  expect.assertions(1);
248
270
  try {
249
271
  await subject();
250
272
  } catch (error: any) {
251
- expect(error.message).toMatch(/only web-components can be published/);
273
+ expect(error.message).toMatch(message);
252
274
  }
253
275
  });
276
+ }
277
+
278
+ describe('when package is not web component', () => {
279
+ beforeEach(() => jest.mocked(isWebComponent).mockReturnValue(false));
280
+
281
+ itThrowsError(/only web-components can be published/);
282
+ });
283
+
284
+ describe('when metadata.json is missing', () => {
285
+ beforeEach(() => fs.unlinkSync('dist/metadata.json'));
286
+
287
+ itThrowsError(/metadata.json is not present/);
288
+ });
289
+
290
+ describe(`when metadata.json contains no entrypoints`, () => {
291
+ beforeEach(() => {
292
+ const metadataJson = path.join('dist', 'metadata.json');
293
+ const metadata = readJson(metadataJson);
294
+ delete metadata.entrypoints;
295
+ fs.writeFileSync(metadataJson, JSON.stringify(metadata));
296
+ });
297
+
298
+ itThrowsError(/not found in metadata.json/);
299
+ });
300
+
301
+ ['full', 'light'].forEach(name => {
302
+ describe.each(['css', 'js'])(`when ${name}.%s entry point is missing`, key => {
303
+ beforeEach(() => {
304
+ const metadataJson = path.join('dist', 'metadata.json');
305
+ const metadata = readJson(metadataJson);
306
+ delete metadata.entrypoints[name][key];
307
+ fs.writeFileSync(metadataJson, JSON.stringify(metadata));
308
+ });
309
+
310
+ itThrowsError(new RegExp(`${name}.${key} not found in metadata.json`));
311
+ });
312
+
313
+ describe(`when ${name} JS bundle is missing`, () => {
314
+ beforeEach(() => fs.unlinkSync(`dist/bundle/${name}/main.bundle.js`));
315
+
316
+ itThrowsError(/referenced bundle main.bundle.js was not found/);
317
+ });
318
+
319
+ describe(`when ${name} CSS bundle is missing`, () => {
320
+ beforeEach(() => fs.unlinkSync(`dist/bundle/${name}/main.bundle.css`));
321
+
322
+ itThrowsError(/referenced bundle main.bundle.css was not found/);
323
+ });
254
324
  });
255
325
  });
@@ -94,6 +94,25 @@ describe(`[startup] ${MFEPublish.name}`, () => {
94
94
  expect.objectContaining({ cmd: 'startup mfe-package-clean' })
95
95
  );
96
96
  });
97
+
98
+ type ArgumentName = keyof typeof args;
99
+ const testArgs: { name: ArgumentName; value: any; expected?: string }[] = [
100
+ { name: 'count', value: 10 },
101
+ { name: 'branch', value: 'foo-123' },
102
+ { name: 'branch', value: true, expected: '--branch' },
103
+ ];
104
+
105
+ describe.each(testArgs)('with "{$name: $value}"', ({ name, value, expected }) => {
106
+ beforeEach(() => (args[name] = value));
107
+
108
+ test(`runs clean with "${expected ?? `--${name} ${value}`}"`, async () => {
109
+ await subject();
110
+
111
+ expect(lernaExec).toHaveBeenCalledWith(
112
+ expect.objectContaining({ '--': [expected ?? `--${name} ${value}`] })
113
+ );
114
+ });
115
+ });
97
116
  });
98
117
 
99
118
  type ArgumentName = keyof typeof args;
@@ -105,7 +124,6 @@ describe(`[startup] ${MFEPublish.name}`, () => {
105
124
  { name: 'dry', value: true, expected: '--dry' },
106
125
  { name: 'force', value: true, expected: '--force' },
107
126
  { name: 'registry', value: 'https://foo' },
108
- { name: 'count', value: 42 },
109
127
  ];
110
128
 
111
129
  describe.each(testArgs)('with "{$name: $value}"', ({ name, value, expected }) => {
@@ -30,10 +30,14 @@ describe(`[startup] ${Tests.name}`, () => {
30
30
  });
31
31
 
32
32
  describe('when the command fails', () => {
33
+ const exitCode = process.exitCode;
34
+
33
35
  beforeEach(() => {
34
36
  jest.mocked(runCLI).mockResolvedValue({ results: { success: false } } as any);
35
37
  });
36
38
 
39
+ afterAll(() => (process.exitCode = exitCode));
40
+
37
41
  test('sets process execCode to 1', async () => {
38
42
  await subject();
39
43
 
@@ -7,7 +7,9 @@ import { Init } from './init';
7
7
  import { Install } from './install';
8
8
  import { KendoUILicense } from './kendo-ui-license';
9
9
  import { Lint } from './lint';
10
- import { MFEPackageClean, MFEPackagePublish, MFEPublish } from './mfe-publish';
10
+ import { MFEPackageClean } from './mfe-package-clean';
11
+ import { MFEPackagePublish } from './mfe-package-publish';
12
+ import { MFEPublish } from './mfe-publish';
11
13
  import { PreparePackage } from './prepare-package';
12
14
  import { Start } from './start';
13
15
  import { StylesCheck } from './styles-check';
@@ -1,21 +1,22 @@
1
1
  import fs from 'fs';
2
- import cpx from 'cpx2';
3
- import util from 'util';
4
2
  import path from 'path';
5
3
 
6
4
  import { log, logErrors } from '../../utils';
5
+ import { runCommand, runCommandOutput } from '../utils/cli-os';
7
6
  import { Command } from './';
8
7
 
9
8
  interface Args {
10
- react17?: boolean;
11
9
  output?: string;
12
10
  }
13
11
 
12
+ const webUrl = 'https://github.com/servicetitan/frontend-example.git';
13
+ const sshUrl = 'git@github.com:servicetitan/frontend-example.git';
14
+
14
15
  export class Init implements Command {
15
16
  constructor(private args: Args) {}
16
17
 
17
18
  description() {
18
- return 'generate empty project';
19
+ return 'create example project';
19
20
  }
20
21
 
21
22
  @logErrors
@@ -25,18 +26,47 @@ export class Init implements Command {
25
26
  fs.mkdirSync(destination, { recursive: true });
26
27
  } else if (!fs.lstatSync(destination).isDirectory()) {
27
28
  throw new Error(`${destination} is not a directory`);
29
+ } else if (fs.readdirSync(destination).length !== 0) {
30
+ throw new Error(`${destination} is not an empty directory`);
28
31
  }
29
32
 
30
- await copyFiles('template', destination);
33
+ const gitUrls = [webUrl, sshUrl];
34
+ if (!!process.env.CI && !!process.env.GITHUB_TOKEN) {
35
+ gitUrls.unshift(
36
+ webUrl.replace('github.com', `oauth2:${process.env.GITHUB_TOKEN}@github.com`)
37
+ );
38
+ }
31
39
 
32
- if (!this.args.react17) {
33
- await copyFiles('template-react18', destination);
40
+ for await (const url of gitUrls) {
41
+ if (await cloneRepo(url, destination)) {
42
+ log.info(`copied example project to ${destination}`);
43
+ return;
44
+ }
34
45
  }
35
46
 
36
- log.info(`copied${this.args.react17 ? ' React 17' : ''} template to ${destination}`);
47
+ if (!gitUrls.some(isReachable)) {
48
+ throw new Error('could not read servicetitan/frontend-example repository');
49
+ }
37
50
  }
38
51
  }
39
52
 
40
- async function copyFiles(from: string, to: string) {
41
- await util.promisify(cpx.copy)(path.resolve(__dirname, `../../../${from}/**/{.*,*,.*/*}`), to);
53
+ async function cloneRepo(url: string, destination: string) {
54
+ try {
55
+ await runCommand(`git clone -q ${url} ${destination}`, { quiet: true });
56
+ } catch (e) {
57
+ return false;
58
+ }
59
+ fs.rmSync(path.join(destination, '.git'), { recursive: true, force: true });
60
+ fs.rmSync(path.join(destination, '.github', 'CODEOWNERS'));
61
+ fs.rmSync(path.join(destination, 'package-lock.json'));
62
+ return true;
63
+ }
64
+
65
+ function isReachable(url: string) {
66
+ try {
67
+ runCommandOutput(`git ls-remote -qt ${url}`, { quiet: true });
68
+ } catch (e) {
69
+ return false;
70
+ }
71
+ return true;
42
72
  }
@@ -0,0 +1,143 @@
1
+ import { isWebComponent, log, logErrors, readJson } from '../../utils';
2
+ import { gitGetBranch } from '../utils/cli-git';
3
+ import { npmGetPackageVersionDates, npmUnpublish } from '../utils/cli-npm';
4
+ import { getBranchesConfigs } from '../../utils/get-branch-configs';
5
+ import { Command } from './types';
6
+
7
+ export interface ArgsPackageClean {
8
+ branch?: string | true;
9
+ count?: number;
10
+ }
11
+
12
+ export class MFEPackageClean implements Command {
13
+ constructor(private args: ArgsPackageClean) {}
14
+
15
+ description() {
16
+ return undefined;
17
+ }
18
+
19
+ @logErrors
20
+ async execute() {
21
+ if (!isWebComponent()) {
22
+ throw new Error('only web-components can be cleaned');
23
+ }
24
+
25
+ const data = this.getCleanData();
26
+ const packageJson = readJson('package.json');
27
+ const packageName = packageJson.name;
28
+ const branchedVersions = this.getBranchedVersions(packageName, data.registry);
29
+
30
+ log.info(
31
+ `branched versions (${data.count}):`,
32
+ JSON.stringify(branchedVersions, undefined, 4)
33
+ );
34
+
35
+ const branchedVersionsToClean: Record<string, [string, Date][]> = {};
36
+
37
+ for (const branch of Object.keys(branchedVersions)) {
38
+ // limit branches for now
39
+ if (!branchedVersions[branch] || !data.branches.includes(branch)) {
40
+ continue;
41
+ }
42
+
43
+ branchedVersions[branch].sort(([, adt], [, bdt]) => (adt > bdt ? -1 : 1));
44
+ branchedVersionsToClean[branch] = branchedVersions[branch].slice(data.count);
45
+ }
46
+
47
+ log.info(
48
+ 'found versions for unpublish:',
49
+ JSON.stringify(branchedVersionsToClean, undefined, 4)
50
+ );
51
+
52
+ const unVersions = Object.keys(branchedVersionsToClean).reduce(
53
+ (out, br) => [...out, ...branchedVersionsToClean[br].map(([v]) => v)],
54
+ []
55
+ );
56
+
57
+ for (const version of unVersions) {
58
+ try {
59
+ // eslint-disable-next-line no-await-in-loop
60
+ await npmUnpublish(data.registry, packageName, version);
61
+ } catch {
62
+ log.error(`error while removing ${packageName} version ${version}`);
63
+ }
64
+ }
65
+ }
66
+
67
+ private getCleanData(): {
68
+ count: number;
69
+ registry: string;
70
+ branches: string[];
71
+ } {
72
+ let count = this.args.count;
73
+ if (!count) {
74
+ count = 5;
75
+ }
76
+
77
+ let branches: string[];
78
+
79
+ if (this.args.branch === true) {
80
+ branches = [gitGetBranch()];
81
+ } else if (typeof this.args.branch === 'string') {
82
+ branches = [this.args.branch];
83
+ } else {
84
+ branches = Object.keys(getBranchesConfigs());
85
+ }
86
+
87
+ const registry = 'https://verdaccio.servicetitan.com';
88
+
89
+ return { count, registry, branches };
90
+ }
91
+
92
+ private getBranchedVersions(packageName: string, registry: string) {
93
+ const versions = npmGetPackageVersionDates(registry, packageName);
94
+ const branchedVersions: Record<string, [string, Date][]> = {};
95
+ const unknownVersions: string[] = [];
96
+
97
+ const addVersion = (branch: string, version: string, dt: Date) => {
98
+ if (!branchedVersions[branch]) {
99
+ branchedVersions[branch] = [];
100
+ }
101
+
102
+ branchedVersions[branch].push([version, dt]);
103
+ };
104
+
105
+ for (const [version, dt] of versions) {
106
+ if (!version.startsWith('0.0.0-')) {
107
+ continue;
108
+ }
109
+
110
+ const buildVersion = version.replace('0.0.0-', '');
111
+
112
+ if (/^(\d+)\.(\d+)\.(\d+)$/.test(buildVersion)) {
113
+ // master version generated by nerdbank versioning
114
+ addVersion('master', version, dt);
115
+ continue;
116
+ }
117
+
118
+ const match1 = buildVersion.match(/^(\d+)\.(\d+)\.(\d+)-([\dA-Za-z-]+).([\dA-Za-z]+)$/);
119
+
120
+ if (match1?.length) {
121
+ // branch version generated by nerdbank versioning
122
+ addVersion(match1[4], version, dt);
123
+ continue;
124
+ }
125
+
126
+ const match2 = buildVersion.match(/^([\dA-Za-z-]+).([\dA-Za-z]+)$/);
127
+
128
+ if (match2?.length) {
129
+ // branch version generated by mfe-publisher versioning
130
+ addVersion(match2[1], version, dt);
131
+ continue;
132
+ }
133
+
134
+ unknownVersions.push(version);
135
+ }
136
+
137
+ if (unknownVersions.length) {
138
+ log.info('unknown versions:', unknownVersions.join());
139
+ }
140
+
141
+ return branchedVersions;
142
+ }
143
+ }