@servicetitan/startup 34.3.0 → 35.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 (91) hide show
  1. package/dist/cli/commands/get-command.d.ts.map +1 -1
  2. package/dist/cli/commands/get-command.js +2 -0
  3. package/dist/cli/commands/get-command.js.map +1 -1
  4. package/dist/cli/commands/mfe-package-publish.d.ts +10 -0
  5. package/dist/cli/commands/mfe-package-publish.d.ts.map +1 -1
  6. package/dist/cli/commands/mfe-package-publish.js +12 -0
  7. package/dist/cli/commands/mfe-package-publish.js.map +1 -1
  8. package/dist/cli/commands/mfe-package-rollback.d.ts +10 -1
  9. package/dist/cli/commands/mfe-package-rollback.d.ts.map +1 -1
  10. package/dist/cli/commands/mfe-package-rollback.js +15 -3
  11. package/dist/cli/commands/mfe-package-rollback.js.map +1 -1
  12. package/dist/cli/commands/mfe-publish.d.ts.map +1 -1
  13. package/dist/cli/commands/mfe-publish.js +10 -4
  14. package/dist/cli/commands/mfe-publish.js.map +1 -1
  15. package/dist/cli/commands/mfe-purge-cache.d.ts +57 -0
  16. package/dist/cli/commands/mfe-purge-cache.d.ts.map +1 -0
  17. package/dist/cli/commands/mfe-purge-cache.js +101 -0
  18. package/dist/cli/commands/mfe-purge-cache.js.map +1 -0
  19. package/dist/cli/commands/{utils.d.ts → utils/build-rollback-tag.d.ts} +1 -2
  20. package/dist/cli/commands/utils/build-rollback-tag.d.ts.map +1 -0
  21. package/dist/cli/commands/{utils.js → utils/build-rollback-tag.js} +1 -5
  22. package/dist/cli/commands/utils/build-rollback-tag.js.map +1 -0
  23. package/dist/cli/commands/utils/constants.d.ts +2 -0
  24. package/dist/cli/commands/utils/constants.d.ts.map +1 -0
  25. package/dist/cli/commands/utils/constants.js +13 -0
  26. package/dist/cli/commands/utils/constants.js.map +1 -0
  27. package/dist/cli/commands/utils/index.d.ts +4 -0
  28. package/dist/cli/commands/utils/index.d.ts.map +1 -0
  29. package/dist/cli/commands/utils/index.js +22 -0
  30. package/dist/cli/commands/utils/index.js.map +1 -0
  31. package/dist/cli/commands/utils/purge-cache.d.ts +6 -0
  32. package/dist/cli/commands/utils/purge-cache.d.ts.map +1 -0
  33. package/dist/cli/commands/utils/purge-cache.js +21 -0
  34. package/dist/cli/commands/utils/purge-cache.js.map +1 -0
  35. package/dist/cli/utils/cli-os.js +1 -1
  36. package/dist/cli/utils/cli-os.js.map +1 -1
  37. package/dist/cypress-config/index.d.ts +2 -0
  38. package/dist/cypress-config/index.d.ts.map +1 -0
  39. package/dist/cypress-config/index.js +20 -0
  40. package/dist/cypress-config/index.js.map +1 -0
  41. package/dist/cypress-config/webpack-config.d.ts +4 -0
  42. package/dist/cypress-config/webpack-config.d.ts.map +1 -0
  43. package/dist/cypress-config/webpack-config.js +76 -0
  44. package/dist/cypress-config/webpack-config.js.map +1 -0
  45. package/dist/storybook-config/swc.js +1 -1
  46. package/dist/storybook-config/swc.js.map +1 -1
  47. package/dist/utils/get-base-tsconfig.d.ts +2 -0
  48. package/dist/utils/get-base-tsconfig.d.ts.map +1 -0
  49. package/dist/utils/get-base-tsconfig.js +21 -0
  50. package/dist/utils/get-base-tsconfig.js.map +1 -0
  51. package/dist/utils/get-configuration.d.ts +1 -0
  52. package/dist/utils/get-configuration.d.ts.map +1 -1
  53. package/dist/utils/get-configuration.js +1 -0
  54. package/dist/utils/get-configuration.js.map +1 -1
  55. package/dist/utils/get-tsconfig-with-fallback.d.ts +6 -0
  56. package/dist/utils/get-tsconfig-with-fallback.d.ts.map +1 -0
  57. package/dist/utils/get-tsconfig-with-fallback.js +24 -0
  58. package/dist/utils/get-tsconfig-with-fallback.js.map +1 -0
  59. package/dist/utils/index.d.ts +2 -0
  60. package/dist/utils/index.d.ts.map +1 -1
  61. package/dist/utils/index.js +2 -0
  62. package/dist/utils/index.js.map +1 -1
  63. package/package.json +23 -21
  64. package/src/cli/commands/__tests__/mfe-package-publish.test.ts +27 -1
  65. package/src/cli/commands/__tests__/mfe-package-rollback.test.ts +31 -0
  66. package/src/cli/commands/__tests__/mfe-publish.test.ts +4 -2
  67. package/src/cli/commands/__tests__/mfe-purge-cache.test.ts +141 -0
  68. package/src/cli/commands/get-command.ts +2 -0
  69. package/src/cli/commands/mfe-package-publish.ts +11 -1
  70. package/src/cli/commands/mfe-package-rollback.ts +15 -5
  71. package/src/cli/commands/mfe-publish.ts +14 -3
  72. package/src/cli/commands/mfe-purge-cache.ts +75 -0
  73. package/src/cli/commands/utils/__tests__/purge-cache.test.ts +40 -0
  74. package/src/cli/commands/{utils.ts → utils/build-rollback-tag.ts} +0 -1
  75. package/src/cli/commands/utils/constants.ts +1 -0
  76. package/src/cli/commands/utils/index.ts +3 -0
  77. package/src/cli/commands/utils/purge-cache.ts +13 -0
  78. package/src/cli/utils/cli-os.ts +1 -1
  79. package/src/cypress-config/__tests__/webpack-config.test.ts +124 -0
  80. package/src/cypress-config/index.ts +1 -0
  81. package/src/cypress-config/webpack-config.ts +58 -0
  82. package/src/storybook-config/__tests__/swc.test.ts +11 -0
  83. package/src/storybook-config/swc.ts +2 -2
  84. package/src/utils/__tests__/get-tsconfig-with-fallback.test.ts +23 -0
  85. package/src/utils/__tests__/get-tsconfig.test.ts +4 -4
  86. package/src/utils/get-base-tsconfig.ts +5 -0
  87. package/src/utils/get-configuration.ts +1 -0
  88. package/src/utils/get-tsconfig-with-fallback.ts +12 -0
  89. package/src/utils/index.ts +2 -0
  90. package/dist/cli/commands/utils.d.ts.map +0 -1
  91. package/dist/cli/commands/utils.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/startup",
3
- "version": "34.3.0",
3
+ "version": "35.1.0",
4
4
  "description": "CLI to create multi-package Lerna projects with TypeScript and React",
5
5
  "homepage": "https://docs.st.dev/docs/frontend/uikit/startup",
6
6
  "repository": {
@@ -9,8 +9,8 @@
9
9
  "directory": "packages/startup"
10
10
  },
11
11
  "engines": {
12
- "node": ">=20.19 <21 || >=22.17",
13
- "npm": ">=9"
12
+ "node": ">=22.17",
13
+ "npm": ">=10"
14
14
  },
15
15
  "sideEffects": false,
16
16
  "main": "./dist/index.js",
@@ -20,6 +20,7 @@
20
20
  "default": "./dist/index.js",
21
21
  "types": "./dist/index.d.ts"
22
22
  },
23
+ "./cypress-config": "./dist/cypress-config/index.js",
23
24
  "./jest-preset": "./jest/jest-preset.js",
24
25
  "./jest-resolver": "./dist/jest/resolver.js",
25
26
  "./jest-svg-transformer": "./dist/jest/svg-transformer.js",
@@ -53,19 +54,19 @@
53
54
  },
54
55
  "dependencies": {
55
56
  "@babel/preset-env": "~7.29.0",
56
- "@jest/core": "~30.2.0",
57
- "@jest/types": "~30.2.0",
57
+ "@jest/core": "~30.3.0",
58
+ "@jest/types": "~30.3.0",
58
59
  "@jsdevtools/coverage-istanbul-loader": "^3.0.5",
59
- "@servicetitan/eslint-config": "34.3.0",
60
- "@servicetitan/install": "34.3.0",
61
- "@servicetitan/startup-utils": "34.3.0",
62
- "@servicetitan/stylelint-config": "34.3.0",
60
+ "@servicetitan/eslint-config": "35.1.0",
61
+ "@servicetitan/install": "35.1.0",
62
+ "@servicetitan/startup-utils": "35.1.0",
63
+ "@servicetitan/stylelint-config": "35.1.0",
63
64
  "@svgr/webpack": "^8.1.0",
64
65
  "@swc/cli": "^0.5.0",
65
66
  "@swc/core": "1.15.18",
66
67
  "@types/debug": "^4.1.12",
67
68
  "@types/jest": "~30.0.0",
68
- "@vitest/coverage-v8": "^4.0.18",
69
+ "@vitest/coverage-v8": "^4.1.0",
69
70
  "chalk": "~4.1.2",
70
71
  "cli-table3": "^0.6.5",
71
72
  "cpx2": "8.0.0",
@@ -80,40 +81,41 @@
80
81
  "html-webpack-plugin": "~5.6.6",
81
82
  "html-webpack-tags-plugin": "^3.0.2",
82
83
  "identity-obj-proxy": "~3.0.0",
83
- "jest": "~30.2.0",
84
- "jest-circus": "~30.2.0",
85
- "jest-environment-jsdom": "^30.2.0",
84
+ "jest": "~30.3.0",
85
+ "jest-circus": "~30.3.0",
86
+ "jest-environment-jsdom": "^30.3.0",
86
87
  "jest-fetch-mock": "~3.0.3",
87
88
  "json5": "^2.2.3",
88
89
  "lerna": "~9.0.5",
89
90
  "less": "~4.5.1",
90
- "less-loader": "~12.3.1",
91
+ "less-loader": "~12.3.2",
91
92
  "less-plugin-npm-import": "~2.1.0",
92
93
  "lodash.kebabcase": "^4.1.1",
93
94
  "lodash.memoize": "^4.1.2",
94
- "memfs": "~4.56.10",
95
- "mini-css-extract-plugin": "~2.10.0",
95
+ "memfs": "~4.56.11",
96
+ "mini-css-extract-plugin": "~2.10.1",
96
97
  "moment-locales-webpack-plugin": "~1.2.0",
97
98
  "multimatch": "~8.0.0",
98
99
  "patch-package": "^8.0.1",
99
100
  "portfinder": "~1.0.38",
100
101
  "postcss": "~8.5.8",
101
102
  "prettier": "~3.8.1",
102
- "sass": "~1.97.3",
103
+ "sass": "~1.98.0",
103
104
  "sass-loader": "~16.0.7",
104
105
  "semver": "~7.7.4",
105
106
  "source-map-loader": "~5.0.0",
106
107
  "style-loader": "~4.0.0",
107
108
  "stylelint": "~16.26.1",
109
+ "swc-loader": "^0.2.7",
108
110
  "terminal-link": "^5.0.0",
109
- "terser-webpack-plugin": "^5.3.16",
111
+ "terser-webpack-plugin": "^5.4.0",
110
112
  "ts-jest": "29.4.6",
111
113
  "ts-node": "~10.9.2",
112
114
  "typed-css-modules": "~0.9.1",
113
115
  "typescript": "5.9.3",
114
- "vitest": "^4.0.18",
116
+ "vitest": "^4.1.0",
115
117
  "webpack": "~5.105.4",
116
- "webpack-assets-manifest": "~6.5.0",
118
+ "webpack-assets-manifest": "~6.5.1",
117
119
  "webpack-bundle-analyzer": "^5.2.0",
118
120
  "webpack-dev-server": "~5.2.3",
119
121
  "webpack-filter-warnings-plugin": "~1.2.1",
@@ -144,5 +146,5 @@
144
146
  "cli": {
145
147
  "webpack": false
146
148
  },
147
- "gitHead": "0ca13bccd0abdbcbda28f08829ef38bde4caa927"
149
+ "gitHead": "f1d49b43f27789cf5cd4bea3e065cd9e33bf5876"
148
150
  }
@@ -12,7 +12,7 @@ import {
12
12
  runCommand,
13
13
  } from '../../utils';
14
14
  import { MFEPackagePublish } from '../mfe-package-publish';
15
- import { ROLLBACK_TAG_SUFFIX } from '../utils';
15
+ import { purgeCache, ROLLBACK_TAG_SUFFIX } from '../utils';
16
16
 
17
17
  jest.mock('fs', () => fs);
18
18
 
@@ -32,6 +32,10 @@ jest.mock('../../utils', () => ({
32
32
  npmView: jest.fn(),
33
33
  runCommand: jest.fn(),
34
34
  }));
35
+ jest.mock('../utils', () => ({
36
+ ...jest.requireActual('../utils'),
37
+ purgeCache: jest.fn(),
38
+ }));
35
39
 
36
40
  describe(`[startup] ${MFEPackagePublish.name}`, () => {
37
41
  const defaultRegistry = 'https://verdaccio.servicetitan.com';
@@ -127,6 +131,12 @@ describe(`[startup] ${MFEPackagePublish.name}`, () => {
127
131
  );
128
132
  });
129
133
 
134
+ test('purges cache after publishing', async () => {
135
+ await subject();
136
+
137
+ expect(purgeCache).toHaveBeenCalledWith({ packageName, tag });
138
+ });
139
+
130
140
  test('gets current tagged versions before publishing', async () => {
131
141
  await subject();
132
142
 
@@ -196,6 +206,14 @@ describe(`[startup] ${MFEPackagePublish.name}`, () => {
196
206
  });
197
207
  });
198
208
 
209
+ function itDoesNotPurgeCache() {
210
+ test('does not purge cache', async () => {
211
+ await subject();
212
+
213
+ expect(purgeCache).not.toHaveBeenCalled();
214
+ });
215
+ }
216
+
199
217
  describe('with "dry" argument', () => {
200
218
  beforeEach(() => (args.dry = true));
201
219
 
@@ -239,6 +257,8 @@ describe(`[startup] ${MFEPackagePublish.name}`, () => {
239
257
  `npx startup upload-sourcemaps --dry --releaseVersion=${buildPackageVersion()}`
240
258
  );
241
259
  });
260
+
261
+ itDoesNotPurgeCache();
242
262
  });
243
263
 
244
264
  function itDoesNotUploadSourcemaps() {
@@ -257,6 +277,12 @@ describe(`[startup] ${MFEPackagePublish.name}`, () => {
257
277
  itDoesNotUploadSourcemaps();
258
278
  });
259
279
 
280
+ describe('with "purgeCache: false"', () => {
281
+ beforeEach(() => (args.purgeCache = false));
282
+
283
+ itDoesNotPurgeCache();
284
+ });
285
+
260
286
  describe('when branch configuration includes "uploadSourcemaps: false"', () => {
261
287
  beforeEach(() =>
262
288
  vol.fromJSON({
@@ -1,6 +1,7 @@
1
1
  import { log, readJson } from '../../../utils';
2
2
  import { npmTagVersion, npmView } from '../../utils';
3
3
  import { MFEPackageRollback } from '../mfe-package-rollback';
4
+ import { purgeCache } from '../utils';
4
5
 
5
6
  jest.mock('../../../utils', () => ({
6
7
  ...jest.requireActual('../../../utils'),
@@ -16,6 +17,10 @@ jest.mock('../../utils', () => ({
16
17
  npmTagVersion: jest.fn(),
17
18
  npmView: jest.fn(),
18
19
  }));
20
+ jest.mock('../utils', () => ({
21
+ ...jest.requireActual('../utils'),
22
+ purgeCache: jest.fn(),
23
+ }));
19
24
 
20
25
  describe(`[startup] ${MFEPackageRollback.name}`, () => {
21
26
  const registry = 'https://verdaccio.servicetitan.com';
@@ -98,6 +103,12 @@ describe(`[startup] ${MFEPackageRollback.name}`, () => {
98
103
  });
99
104
  });
100
105
 
106
+ test('purges cache after rollback', async () => {
107
+ await subject();
108
+
109
+ expect(purgeCache).toHaveBeenCalledWith({ packageName, tag });
110
+ });
111
+
101
112
  describe('when package is not found in registry', () => {
102
113
  beforeEach(() => {
103
114
  jest.mocked(npmView).mockReturnValue(undefined);
@@ -114,6 +125,14 @@ describe(`[startup] ${MFEPackageRollback.name}`, () => {
114
125
  itReportsError(`No rollback version found for tag "${tag}" (looking for "${rollbackTag}")`);
115
126
  });
116
127
 
128
+ function itDoesNotPurgeCache() {
129
+ test('does not purge cache', async () => {
130
+ await subject();
131
+
132
+ expect(purgeCache).not.toHaveBeenCalled();
133
+ });
134
+ }
135
+
117
136
  describe('when tag and rollback tag both point to the same version', () => {
118
137
  beforeEach(() => {
119
138
  packageInfo['dist-tags'] = {
@@ -130,6 +149,8 @@ describe(`[startup] ${MFEPackageRollback.name}`, () => {
130
149
  );
131
150
  expect(npmTagVersion).not.toHaveBeenCalled();
132
151
  });
152
+
153
+ itDoesNotPurgeCache();
133
154
  });
134
155
 
135
156
  describe('when tag does not exist', () => {
@@ -190,5 +211,15 @@ describe(`[startup] ${MFEPackageRollback.name}`, () => {
190
211
  )
191
212
  );
192
213
  });
214
+
215
+ itDoesNotPurgeCache();
216
+ });
217
+
218
+ describe('with purgeCache: false', () => {
219
+ beforeEach(() => {
220
+ args.purgeCache = false;
221
+ });
222
+
223
+ itDoesNotPurgeCache();
193
224
  });
194
225
  });
@@ -139,11 +139,13 @@ describe(`[startup] ${MFEPublish.name}`, () => {
139
139
  const testArgs: { name: ArgumentName; value: any; expected?: string | string[] }[] = [
140
140
  { name: 'build', value: true },
141
141
  { name: 'branch', value: 'foo-123' },
142
- { name: 'tag', value: 'foo' },
143
- { name: 'tag', value: true, expected: [] }, // check ignores obsolete --tag with no name
144
142
  { name: 'dry', value: true, expected: '--dry' },
145
143
  { name: 'force', value: true, expected: '--force' },
144
+ { name: 'purgeCache', value: true, expected: '--purge-cache' },
145
+ { name: 'purgeCache', value: false, expected: '--no-purge-cache' },
146
146
  { name: 'registry', value: 'https://foo' },
147
+ { name: 'tag', value: 'foo' },
148
+ { name: 'tag', value: true, expected: [] }, // check ignores obsolete --tag with no name
147
149
  { name: 'trackHistory', value: true, expected: '--track-history' },
148
150
  { name: 'trackHistory', value: false, expected: '--no-track-history' },
149
151
  { name: 'uploadSourcemaps', value: true, expected: '--upload-sourcemaps' },
@@ -0,0 +1,141 @@
1
+ import { log } from '../../../utils';
2
+ import { MFEPurgeCache, PurgeResponse } from '../mfe-purge-cache';
3
+
4
+ jest.mock('../../../utils', () => ({
5
+ ...jest.requireActual('../../../utils'),
6
+ log: { debug: jest.fn(), info: jest.fn(), warning: jest.fn() },
7
+ }));
8
+
9
+ describe(`[startup] ${MFEPurgeCache.name}`, () => {
10
+ const host = 'https://unpkg.example.com';
11
+ const token = 'purge-token';
12
+ const tag = 'prod';
13
+ const packageName = '@servicetitan/foo';
14
+ const version = '0.0.0-master.abc1223';
15
+ const location = `/${packageName}@${version}/dist/metadata.json`;
16
+
17
+ let args: ConstructorParameters<typeof MFEPurgeCache>[0];
18
+ let fetchSpy: jest.SpyInstance;
19
+ let fetchResponse: any;
20
+ let responseJson: PurgeResponse;
21
+
22
+ beforeEach(() => {
23
+ process.env.UNPKG_PURGE_TOKEN = token;
24
+ args = { host, packageName, tag };
25
+ fetchResponse = {
26
+ ok: true,
27
+ json: jest.fn().mockImplementation(() => Promise.resolve(responseJson)),
28
+ };
29
+ responseJson = [{ pod: '10.36.110.200', status: 302, cache: 'BYPASS', location }];
30
+
31
+ jest.clearAllMocks();
32
+ jest.spyOn(process.stdout, 'write').mockImplementation(jest.fn()); // suppress error output
33
+ fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(() => fetchResponse);
34
+ });
35
+
36
+ afterEach(() => delete process.env.UNPKG_PURGE_TOKEN);
37
+
38
+ const subject = () => new MFEPurgeCache(args).execute();
39
+
40
+ test('sends POST request with X-Purge-Token header', async () => {
41
+ await subject();
42
+
43
+ expect(fetchSpy).toHaveBeenCalledWith(
44
+ `${host}/_purge/${packageName}@${tag}/dist/metadata.json`,
45
+ { method: 'POST', headers: { 'X-Purge-Token': token } }
46
+ );
47
+ });
48
+
49
+ test('logs progress', async () => {
50
+ await subject();
51
+
52
+ expect(log.info).toHaveBeenCalledWith(`Purging cache for ${packageName}@${tag}`);
53
+ });
54
+
55
+ test('logs the redirect location from the response', async () => {
56
+ await subject();
57
+
58
+ expect(log.info).toHaveBeenCalledWith(
59
+ `Cache purged: ${packageName}@${tag}/dist/metadata.json -> ${location.replace(/^\//, '')}`
60
+ );
61
+ });
62
+
63
+ describe('when request fails', () => {
64
+ beforeEach(() => {
65
+ fetchResponse = { ok: false, status: 403, statusText: 'Forbidden' };
66
+ });
67
+
68
+ test('throws error', () => {
69
+ expect(subject).rejects.toThrow(
70
+ `Purge request failed: ${fetchResponse.status} ${fetchResponse.statusText}`
71
+ );
72
+ });
73
+ });
74
+
75
+ function itLogsNullLocation() {
76
+ test('logs null location', async () => {
77
+ await subject();
78
+
79
+ expect(log.info).toHaveBeenCalledWith(expect.stringMatching(/-> null$/));
80
+ });
81
+ }
82
+
83
+ describe('when response location is null', () => {
84
+ beforeEach(() => (responseJson[0].location = null));
85
+
86
+ itLogsNullLocation();
87
+ });
88
+
89
+ describe('when response is empty', () => {
90
+ beforeEach(() => (responseJson = []));
91
+
92
+ itLogsNullLocation();
93
+ });
94
+
95
+ describe('when response is unexpected', () => {
96
+ beforeEach(() => (responseJson = {} as any));
97
+
98
+ itLogsNullLocation();
99
+ });
100
+
101
+ describe('when host has trailing slash', () => {
102
+ beforeEach(() => {
103
+ args.host = `${host}/`;
104
+ });
105
+
106
+ test('strips trailing slash from host', async () => {
107
+ await subject();
108
+
109
+ expect(fetchSpy).toHaveBeenCalledWith(
110
+ `${host}/_purge/${packageName}@${tag}/dist/metadata.json`,
111
+ expect.anything()
112
+ );
113
+ });
114
+ });
115
+
116
+ describe('with --token', () => {
117
+ const cliToken = 'cli-purge-token';
118
+
119
+ beforeEach(() => (args.token = cliToken));
120
+
121
+ test('uses specified token', async () => {
122
+ await subject();
123
+
124
+ expect(fetchSpy).toHaveBeenCalledWith(
125
+ expect.anything(),
126
+ expect.objectContaining({ headers: { 'X-Purge-Token': cliToken } })
127
+ );
128
+ });
129
+ });
130
+
131
+ describe('with no token', () => {
132
+ beforeEach(() => delete process.env.UNPKG_PURGE_TOKEN);
133
+
134
+ test('logs warning and does nothing', async () => {
135
+ await subject();
136
+
137
+ expect(log.warning).toHaveBeenCalledWith('Token is not defined, skipping purge');
138
+ expect(fetchSpy).not.toHaveBeenCalled();
139
+ });
140
+ });
141
+ });
@@ -15,6 +15,7 @@ import { MFEPackageClean } from './mfe-package-clean';
15
15
  import { MFEPackagePublish } from './mfe-package-publish';
16
16
  import { MFEPackageRollback } from './mfe-package-rollback';
17
17
  import { MFEPublish } from './mfe-publish';
18
+ import { MFEPurgeCache } from './mfe-purge-cache';
18
19
  import { PreparePackage } from './prepare-package';
19
20
  import { Review } from './review';
20
21
  import { RunTask } from './run-task';
@@ -42,6 +43,7 @@ const commands: Record<CommandName, Newable<Command>> = {
42
43
  [CommandName['mfe-package-publish']]: MFEPackagePublish,
43
44
  [CommandName['mfe-package-rollback']]: MFEPackageRollback,
44
45
  [CommandName['mfe-publish']]: MFEPublish,
46
+ [CommandName['mfe-purge-cache']]: MFEPurgeCache,
45
47
  [CommandName['prepare-package']]: PreparePackage,
46
48
  [CommandName.review]: Review,
47
49
  [CommandName.start]: Start,
@@ -22,7 +22,7 @@ import {
22
22
  runCommand,
23
23
  } from '../utils';
24
24
  import { Command, CommandOptions } from './types';
25
- import { buildRollbackTag, DEFAULT_MFE_REGISTRY } from './utils';
25
+ import { DEFAULT_MFE_REGISTRY, buildRollbackTag, purgeCache } from './utils';
26
26
 
27
27
  export const mfePackagePublishOptions = {
28
28
  branch: {
@@ -59,6 +59,11 @@ export const mfePackagePublishOptions = {
59
59
  description: 'Upload source maps to Datadog?',
60
60
  defaultDescription: 'true',
61
61
  },
62
+ purgeCache: {
63
+ boolean: true,
64
+ description: 'Purge unpkg cache after publishing?',
65
+ defaultDescription: 'true',
66
+ },
62
67
  } satisfies CommandOptions;
63
68
 
64
69
  interface PublishData {
@@ -100,6 +105,11 @@ export class MFEPackagePublish extends Command<typeof mfePackagePublishOptions>
100
105
 
101
106
  await this.maybePublishPackage(packageJson, data);
102
107
 
108
+ const { dry, tag } = data;
109
+ if (this.args.purgeCache !== false && !dry) {
110
+ purgeCache({ packageName: packageJson.name, tag });
111
+ }
112
+
103
113
  if (data.uploadSourcemaps) {
104
114
  this.uploadSourcemaps(data);
105
115
  }
@@ -1,7 +1,7 @@
1
1
  import { log, logErrors, readJson } from '../../utils';
2
2
  import { DRY_RUN_PREFIX, npmTagVersion, npmView } from '../utils';
3
3
  import { Command, CommandOptions } from './types';
4
- import { buildRollbackTag, DEFAULT_MFE_REGISTRY } from './utils';
4
+ import { DEFAULT_MFE_REGISTRY, buildRollbackTag, purgeCache } from './utils';
5
5
 
6
6
  export const mfePackageRollbackOptions = {
7
7
  dry: {
@@ -18,6 +18,11 @@ export const mfePackageRollbackOptions = {
18
18
  description: 'The tag to rollback to its previous version',
19
19
  required: true,
20
20
  },
21
+ purgeCache: {
22
+ boolean: true,
23
+ description: 'Purge unpkg cache after rolling back?',
24
+ defaultDescription: 'true',
25
+ },
21
26
  } satisfies CommandOptions;
22
27
 
23
28
  interface PackageJson {
@@ -25,8 +30,6 @@ interface PackageJson {
25
30
  }
26
31
 
27
32
  export class MFEPackageRollback extends Command<typeof mfePackageRollbackOptions> {
28
- static readonly description = 'Rollback a package to a previous tagged version';
29
-
30
33
  static readonly options = mfePackageRollbackOptions;
31
34
 
32
35
  private get dryRunPrefix() {
@@ -35,11 +38,16 @@ export class MFEPackageRollback extends Command<typeof mfePackageRollbackOptions
35
38
 
36
39
  @logErrors
37
40
  async execute() {
38
- if (this.args.dry) {
41
+ const { dry, tag } = this.args;
42
+ if (dry) {
39
43
  log.warning('DRY-RUN MODE ENABLED, WILL NOT PERFORM ROLLBACK');
40
44
  }
41
45
 
42
- await this.handleRollback();
46
+ const packageName = await this.handleRollback();
47
+
48
+ if (packageName && this.args.purgeCache !== false && !dry) {
49
+ purgeCache({ packageName, tag });
50
+ }
43
51
  }
44
52
 
45
53
  private async handleRollback() {
@@ -96,5 +104,7 @@ export class MFEPackageRollback extends Command<typeof mfePackageRollbackOptions
96
104
  tag,
97
105
  });
98
106
  }
107
+
108
+ return packageName;
99
109
  }
100
110
  }
@@ -111,17 +111,28 @@ export class MFEPublish extends Command<MFEPublishOptions> {
111
111
  }
112
112
 
113
113
  getPublishOptions() {
114
- const { build, branch, dry, force, registry, tag, uploadSourcemaps, trackHistory } =
115
- this.args;
114
+ const {
115
+ build,
116
+ branch,
117
+ dry,
118
+ force,
119
+ purgeCache,
120
+ registry,
121
+ tag,
122
+ uploadSourcemaps,
123
+ trackHistory,
124
+ } = this.args;
116
125
  return [
117
126
  ...[branch && `--branch ${branch}`],
118
127
  ...[build && `--build ${build}`],
119
128
  ...[dry && '--dry'],
120
129
  ...[force && '--force'],
130
+ ...[purgeCache === true && '--purge-cache'],
131
+ ...[purgeCache === false && '--no-purge-cache'],
121
132
  ...[registry && `--registry ${registry}`],
133
+ ...[typeof tag === 'string' && `--tag ${tag}`],
122
134
  ...[trackHistory === true && '--track-history'],
123
135
  ...[trackHistory === false && '--no-track-history'],
124
- ...[typeof tag === 'string' && `--tag ${tag}`],
125
136
  ...[uploadSourcemaps === true && `--upload-sourcemaps`],
126
137
  ...[uploadSourcemaps === false && `--no-upload-sourcemaps`],
127
138
  ].filter(item => !!item) as string[];
@@ -0,0 +1,75 @@
1
+ import { log, logErrors } from '../../utils';
2
+ import { Command, CommandOptions } from './types';
3
+
4
+ const mfePurgeCacheOptions = {
5
+ host: {
6
+ string: true,
7
+ description: 'Host URL to send purge requests to',
8
+ demandOption: true,
9
+ },
10
+ packageName: {
11
+ string: true,
12
+ description: 'Package name to purge',
13
+ demandOption: true,
14
+ },
15
+ tag: {
16
+ string: true,
17
+ description: 'Package tag to purge',
18
+ demandOption: true,
19
+ },
20
+ token: {
21
+ string: true,
22
+ description: 'X-Purge-Token header value',
23
+ defaultDescription: 'process.env.UNPKG_PURGE_TOKEN',
24
+ },
25
+ } satisfies CommandOptions;
26
+
27
+ export type PurgeResponse = {
28
+ pod: string;
29
+ status: number;
30
+ cache: string | null;
31
+ location: string | null;
32
+ }[];
33
+
34
+ export class MFEPurgeCache extends Command<typeof mfePurgeCacheOptions> {
35
+ static readonly options = mfePurgeCacheOptions;
36
+
37
+ @logErrors
38
+ async execute() {
39
+ const { host: rawHost, packageName, tag } = this.args;
40
+ const host = rawHost.replace(/\/$/, '');
41
+ const token = this.args.token ?? process.env.UNPKG_PURGE_TOKEN;
42
+ const packageNameAndTag = `${packageName}@${tag}`;
43
+ const path = `${packageNameAndTag}/dist/metadata.json`;
44
+
45
+ log.info(`Purging cache for ${packageNameAndTag}`);
46
+
47
+ if (!token) {
48
+ log.warning('Token is not defined, skipping purge');
49
+ return;
50
+ }
51
+
52
+ const response = await fetch(`${host}/_purge/${path}`, {
53
+ method: 'POST',
54
+ headers: { 'X-Purge-Token': token },
55
+ });
56
+
57
+ if (!response.ok) {
58
+ throw new Error(`Purge request failed: ${response.status} ${response.statusText}`);
59
+ }
60
+
61
+ const responseJson = await response.json();
62
+ /* istanbul ignore next: debug only */
63
+ log.debug('mfe-purge-cache', () => JSON.stringify(responseJson, null, 2));
64
+
65
+ log.info(`Cache purged: ${path} -> ${this.getLocation(responseJson)}`);
66
+ }
67
+
68
+ private getLocation(responseJson: PurgeResponse) {
69
+ let location: string | null = null;
70
+ if (Array.isArray(responseJson)) {
71
+ location = responseJson[0]?.location?.replace(/^\//, '') ?? null;
72
+ }
73
+ return location;
74
+ }
75
+ }
@@ -0,0 +1,40 @@
1
+ import { runCommandOutput } from '../../../utils';
2
+ import { purgeCache } from '../purge-cache';
3
+
4
+ jest.mock('../../../utils', () => ({
5
+ ...jest.requireActual('../../../utils'),
6
+ runCommandOutput: jest.fn(),
7
+ }));
8
+
9
+ describe(purgeCache.name, () => {
10
+ let args: Parameters<typeof purgeCache>[0];
11
+ const packageName = '@servicetitan/foo';
12
+ const tag = 'prod';
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ args = { packageName, tag };
17
+ });
18
+
19
+ const subject = () => purgeCache(args);
20
+
21
+ test('runs mfe-purge-cache with host "https://unpkg.servicetitan.com", package name and tag', () => {
22
+ subject();
23
+
24
+ expect(runCommandOutput).toHaveBeenCalledWith(
25
+ `npx startup mfe-purge-cache --host https://unpkg.servicetitan.com --package-name ${packageName} --tag ${tag}`
26
+ );
27
+ });
28
+
29
+ describe('when registry is "https://verdaccio.st.dev"', () => {
30
+ beforeEach(() => (args.registry = 'https://verdaccio.st.dev'));
31
+
32
+ test('runs mfe-purge-cache with host "https://unpkg.st.dev"', () => {
33
+ subject();
34
+
35
+ expect(runCommandOutput).toHaveBeenCalledWith(
36
+ expect.stringContaining(`--host https://unpkg.st.dev`)
37
+ );
38
+ });
39
+ });
40
+ });
@@ -1,4 +1,3 @@
1
- export const DEFAULT_MFE_REGISTRY = 'https://verdaccio.servicetitan.com';
2
1
  export const ROLLBACK_TAG_SUFFIX = '-rollback-1';
3
2
 
4
3
  export function buildRollbackTag(tag: string): string {
@@ -0,0 +1 @@
1
+ export const DEFAULT_MFE_REGISTRY = 'https://verdaccio.servicetitan.com';
@@ -0,0 +1,3 @@
1
+ export * from './build-rollback-tag';
2
+ export * from './constants';
3
+ export * from './purge-cache';