@servicetitan/startup 39.0.1 → 39.2.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 (122) hide show
  1. package/dist/cli/commands/mfe-list.d.ts +0 -2
  2. package/dist/cli/commands/mfe-list.d.ts.map +1 -1
  3. package/dist/cli/commands/mfe-list.js +4 -30
  4. package/dist/cli/commands/mfe-list.js.map +1 -1
  5. package/dist/cli/commands/mfe-package-clean.js +1 -2
  6. package/dist/cli/commands/mfe-package-clean.js.map +1 -1
  7. package/dist/cli/commands/mfe-package-publish.d.ts.map +1 -1
  8. package/dist/cli/commands/mfe-package-publish.js +7 -5
  9. package/dist/cli/commands/mfe-package-publish.js.map +1 -1
  10. package/dist/cli/commands/mfe-package-rollback.d.ts.map +1 -1
  11. package/dist/cli/commands/mfe-package-rollback.js +5 -3
  12. package/dist/cli/commands/mfe-package-rollback.js.map +1 -1
  13. package/dist/cli/commands/mfe-publish.d.ts +1 -0
  14. package/dist/cli/commands/mfe-publish.d.ts.map +1 -1
  15. package/dist/cli/commands/mfe-publish.js +14 -12
  16. package/dist/cli/commands/mfe-publish.js.map +1 -1
  17. package/dist/cli/commands/registry/command-registry.d.ts +13 -4
  18. package/dist/cli/commands/registry/command-registry.d.ts.map +1 -1
  19. package/dist/cli/commands/registry/mfe-list.d.ts +2 -1
  20. package/dist/cli/commands/registry/mfe-list.d.ts.map +1 -1
  21. package/dist/cli/commands/registry/mfe-list.js +3 -1
  22. package/dist/cli/commands/registry/mfe-list.js.map +1 -1
  23. package/dist/cli/commands/registry/mfe-package-clean.d.ts +2 -1
  24. package/dist/cli/commands/registry/mfe-package-clean.d.ts.map +1 -1
  25. package/dist/cli/commands/registry/mfe-package-clean.js +4 -2
  26. package/dist/cli/commands/registry/mfe-package-clean.js.map +1 -1
  27. package/dist/cli/commands/registry/mfe-package-publish.d.ts +2 -1
  28. package/dist/cli/commands/registry/mfe-package-publish.d.ts.map +1 -1
  29. package/dist/cli/commands/registry/mfe-package-publish.js +4 -2
  30. package/dist/cli/commands/registry/mfe-package-publish.js.map +1 -1
  31. package/dist/cli/commands/registry/mfe-package-rollback.d.ts +2 -1
  32. package/dist/cli/commands/registry/mfe-package-rollback.d.ts.map +1 -1
  33. package/dist/cli/commands/registry/mfe-package-rollback.js +4 -2
  34. package/dist/cli/commands/registry/mfe-package-rollback.js.map +1 -1
  35. package/dist/cli/commands/registry/upload-sourcemaps.d.ts +5 -0
  36. package/dist/cli/commands/registry/upload-sourcemaps.d.ts.map +1 -1
  37. package/dist/cli/commands/registry/upload-sourcemaps.js +5 -0
  38. package/dist/cli/commands/registry/upload-sourcemaps.js.map +1 -1
  39. package/dist/cli/commands/review/types.js.map +1 -1
  40. package/dist/cli/commands/upload-sourcemaps.d.ts.map +1 -1
  41. package/dist/cli/commands/upload-sourcemaps.js +4 -7
  42. package/dist/cli/commands/upload-sourcemaps.js.map +1 -1
  43. package/dist/cli/commands/utils/get-mfe-registry.d.ts +4 -0
  44. package/dist/cli/commands/utils/get-mfe-registry.d.ts.map +1 -0
  45. package/dist/cli/commands/utils/get-mfe-registry.js +28 -0
  46. package/dist/cli/commands/utils/get-mfe-registry.js.map +1 -0
  47. package/dist/cli/commands/utils/get-unpkg-host.d.ts +2 -0
  48. package/dist/cli/commands/utils/get-unpkg-host.d.ts.map +1 -0
  49. package/dist/cli/commands/utils/get-unpkg-host.js +18 -0
  50. package/dist/cli/commands/utils/get-unpkg-host.js.map +1 -0
  51. package/dist/cli/commands/utils/index.d.ts +3 -1
  52. package/dist/cli/commands/utils/index.d.ts.map +1 -1
  53. package/dist/cli/commands/utils/index.js +3 -1
  54. package/dist/cli/commands/utils/index.js.map +1 -1
  55. package/dist/cli/commands/utils/login.d.ts +2 -0
  56. package/dist/cli/commands/utils/login.d.ts.map +1 -0
  57. package/dist/cli/commands/utils/login.js +56 -0
  58. package/dist/cli/commands/utils/login.js.map +1 -0
  59. package/dist/cli/commands/utils/purge-cache.d.ts.map +1 -1
  60. package/dist/cli/commands/utils/purge-cache.js +2 -4
  61. package/dist/cli/commands/utils/purge-cache.js.map +1 -1
  62. package/dist/cli/index.js +4 -1
  63. package/dist/cli/index.js.map +1 -1
  64. package/dist/vite/build-escaped-reg-exp.d.ts +2 -0
  65. package/dist/vite/build-escaped-reg-exp.d.ts.map +1 -0
  66. package/dist/vite/build-escaped-reg-exp.js +16 -0
  67. package/dist/vite/build-escaped-reg-exp.js.map +1 -0
  68. package/dist/vite/config/base-config.d.ts.map +1 -1
  69. package/dist/vite/config/base-config.js +7 -2
  70. package/dist/vite/config/base-config.js.map +1 -1
  71. package/dist/vite/config/create-resolve-config.d.ts +6 -0
  72. package/dist/vite/config/create-resolve-config.d.ts.map +1 -0
  73. package/dist/vite/config/create-resolve-config.js +52 -0
  74. package/dist/vite/config/create-resolve-config.js.map +1 -0
  75. package/dist/vite/plugins/css-injector-plugin.d.ts.map +1 -1
  76. package/dist/vite/plugins/css-injector-plugin.js +5 -1
  77. package/dist/vite/plugins/css-injector-plugin.js.map +1 -1
  78. package/dist/vite/plugins/externals-plugin.d.ts.map +1 -1
  79. package/dist/vite/plugins/externals-plugin.js +3 -3
  80. package/dist/vite/plugins/externals-plugin.js.map +1 -1
  81. package/package.json +15 -16
  82. package/src/cli/commands/__tests__/mfe-list.test.ts +25 -56
  83. package/src/cli/commands/__tests__/mfe-package-clean.test.ts +6 -0
  84. package/src/cli/commands/__tests__/mfe-package-publish.test.ts +52 -47
  85. package/src/cli/commands/__tests__/mfe-package-rollback.test.ts +4 -2
  86. package/src/cli/commands/__tests__/mfe-publish.test.ts +51 -14
  87. package/src/cli/commands/__tests__/upload-sourcemaps.test.ts +27 -13
  88. package/src/cli/commands/mfe-list.ts +7 -36
  89. package/src/cli/commands/mfe-package-clean.ts +2 -2
  90. package/src/cli/commands/mfe-package-publish.ts +10 -8
  91. package/src/cli/commands/mfe-package-rollback.ts +6 -5
  92. package/src/cli/commands/mfe-publish.ts +17 -16
  93. package/src/cli/commands/registry/mfe-list.ts +6 -1
  94. package/src/cli/commands/registry/mfe-package-clean.ts +7 -1
  95. package/src/cli/commands/registry/mfe-package-publish.ts +7 -1
  96. package/src/cli/commands/registry/mfe-package-rollback.ts +4 -2
  97. package/src/cli/commands/registry/upload-sourcemaps.ts +1 -0
  98. package/src/cli/commands/upload-sourcemaps.ts +4 -7
  99. package/src/cli/commands/utils/__tests__/get-mfe-registry.test.ts +24 -0
  100. package/src/cli/commands/utils/__tests__/get-unpkg-host.test.ts +32 -0
  101. package/src/cli/commands/utils/__tests__/login.test.ts +122 -0
  102. package/src/cli/commands/utils/__tests__/purge-cache.test.ts +17 -15
  103. package/src/cli/commands/utils/get-mfe-registry.ts +6 -0
  104. package/src/cli/commands/utils/get-unpkg-host.ts +8 -0
  105. package/src/cli/commands/utils/index.ts +3 -1
  106. package/src/cli/commands/utils/login.ts +38 -0
  107. package/src/cli/commands/utils/purge-cache.ts +2 -5
  108. package/src/cli/index.ts +2 -1
  109. package/src/vite/__tests__/build-escaped-reg-exp.test.ts +25 -0
  110. package/src/vite/build-escaped-reg-exp.ts +6 -0
  111. package/src/vite/config/__tests__/base-config.test.ts +1 -1
  112. package/src/vite/config/__tests__/create-resolve-config.test.ts +85 -0
  113. package/src/vite/config/base-config.ts +4 -2
  114. package/src/vite/config/create-resolve-config.ts +43 -0
  115. package/src/vite/plugins/__tests__/css-injector-plugin.test.ts +1 -1
  116. package/src/vite/plugins/css-injector-plugin.ts +6 -1
  117. package/src/vite/plugins/externals-plugin.ts +1 -4
  118. package/dist/cli/commands/utils/constants.d.ts +0 -2
  119. package/dist/cli/commands/utils/constants.d.ts.map +0 -1
  120. package/dist/cli/commands/utils/constants.js +0 -13
  121. package/dist/cli/commands/utils/constants.js.map +0 -1
  122. package/src/cli/commands/utils/constants.ts +0 -1
@@ -1,3 +1,4 @@
1
+ import { isCI } from '@servicetitan/install';
1
2
  import { getPackages, isWebComponent, logErrors, PackageType } from '../../utils';
2
3
  import { lernaExec } from '../utils';
3
4
  import { entry as mfePackageClean } from './registry/mfe-package-clean';
@@ -5,6 +6,7 @@ import { entry as mfePackagePublish } from './registry/mfe-package-publish';
5
6
  import { entry as mfePackageRollback } from './registry/mfe-package-rollback';
6
7
  import type { entry } from './registry/mfe-publish';
7
8
  import { Command } from './types';
9
+ import { getMfeRegistry, login } from './utils';
8
10
 
9
11
  type MFEPublishOptions = (typeof entry)['options'] &
10
12
  (typeof mfePackagePublish)['options'] &
@@ -25,6 +27,10 @@ export class MFEPublish extends Command<{ options: MFEPublishOptions }> {
25
27
  throw new Error('no packages found for publication');
26
28
  }
27
29
 
30
+ if (!isCI()) {
31
+ await login(this.registry);
32
+ }
33
+
28
34
  const isRollback = !!this.args.rollback;
29
35
  const isClean = !!this.args.clean;
30
36
 
@@ -48,26 +54,21 @@ export class MFEPublish extends Command<{ options: MFEPublishOptions }> {
48
54
  });
49
55
  }
50
56
 
57
+ private get registry() {
58
+ return getMfeRegistry(this.args.registry);
59
+ }
60
+
51
61
  getPublishOptions() {
52
- const {
53
- build,
54
- branch,
55
- dry,
56
- force,
57
- purgeCache,
58
- registry,
59
- tag,
60
- uploadSourcemaps,
61
- trackHistory,
62
- } = this.args;
62
+ const { build, branch, dry, force, purgeCache, tag, uploadSourcemaps, trackHistory } =
63
+ this.args;
63
64
  return [
65
+ `--registry ${this.registry}`,
64
66
  ...[branch && `--branch ${branch}`],
65
67
  ...[build && `--build ${build}`],
66
68
  ...[dry && '--dry'],
67
69
  ...[force && '--force'],
68
70
  ...[purgeCache === true && '--purge-cache'],
69
71
  ...[purgeCache === false && '--no-purge-cache'],
70
- ...[registry && `--registry ${registry}`],
71
72
  ...[typeof tag === 'string' && `--tag ${tag}`],
72
73
  ...[trackHistory === true && '--track-history'],
73
74
  ...[trackHistory === false && '--no-track-history'],
@@ -77,22 +78,22 @@ export class MFEPublish extends Command<{ options: MFEPublishOptions }> {
77
78
  }
78
79
 
79
80
  getRollbackOptions() {
80
- const { dry, registry, tag } = this.args;
81
+ const { dry, tag } = this.args;
81
82
  return [
83
+ `--registry ${this.registry}`,
82
84
  ...[dry && '--dry'],
83
- ...[registry && `--registry ${registry}`],
84
85
  ...[typeof tag === 'string' && `--tag ${tag}`],
85
86
  ].filter(item => !!item) as string[];
86
87
  }
87
88
 
88
89
  getCleanOptions() {
89
- const { all, branch, count, dry, registry } = this.args;
90
+ const { all, branch, count, dry } = this.args;
90
91
  return [
92
+ `--registry ${this.registry}`,
91
93
  ...[all && '--all'],
92
94
  ...[typeof branch === 'string' && `--branch ${branch}`],
93
95
  ...[count && `--count ${count}`],
94
96
  ...[dry && '--dry'],
95
- ...[registry && `--registry ${registry}`],
96
97
  ].filter(item => !!item) as string[];
97
98
  }
98
99
  }
@@ -1,4 +1,5 @@
1
1
  import type { CommandEntry } from '../types';
2
+ import { PROD_MFE_REGISTRY } from '../utils/get-mfe-registry';
2
3
  import { defineEntry } from './define-entry';
3
4
 
4
5
  export const entry = defineEntry({
@@ -19,7 +20,11 @@ export const entry = defineEntry({
19
20
  description: 'List only the specified number of published versions',
20
21
  },
21
22
  packageNames: { array: true, string: true, hidden: true },
22
- registry: { string: true, hidden: true },
23
+ registry: {
24
+ string: true,
25
+ description: 'List packages published to the specified registry',
26
+ defaultDescription: PROD_MFE_REGISTRY,
27
+ },
23
28
  tagged: { boolean: true, description: 'List only tagged versions' },
24
29
  },
25
30
  } satisfies CommandEntry);
@@ -1,4 +1,5 @@
1
1
  import type { CommandEntry } from '../types';
2
+ import { getMfeRegistry } from '../utils/get-mfe-registry';
2
3
  import { defineEntry } from './define-entry';
3
4
 
4
5
  export const entry = defineEntry({
@@ -22,6 +23,11 @@ export const entry = defineEntry({
22
23
  boolean: true,
23
24
  description: 'Take no action and only log what would be unpublished',
24
25
  },
25
- registry: { string: true, hidden: true, description: 'Registry url' },
26
+ registry: {
27
+ string: true,
28
+ hidden: false,
29
+ description: 'Registry to remove old versions from',
30
+ defaultDescription: getMfeRegistry(),
31
+ },
26
32
  },
27
33
  } satisfies CommandEntry);
@@ -1,4 +1,5 @@
1
1
  import type { CommandEntry } from '../types';
2
+ import { getMfeRegistry } from '../utils/get-mfe-registry';
2
3
  import { defineEntry } from './define-entry';
3
4
 
4
5
  export const entry = defineEntry({
@@ -21,7 +22,12 @@ export const entry = defineEntry({
21
22
  boolean: true,
22
23
  description: 'Force publishing from an unrecognized branch',
23
24
  },
24
- registry: { string: true, hidden: true, description: 'Registry url' },
25
+ registry: {
26
+ string: true,
27
+ hidden: false,
28
+ description: 'Registry to publish to',
29
+ defaultDescription: getMfeRegistry(),
30
+ },
25
31
  trackHistory: {
26
32
  boolean: true,
27
33
  description: 'Tag the previous release, to support rollbacks',
@@ -1,4 +1,5 @@
1
1
  import type { CommandEntry } from '../types';
2
+ import { getMfeRegistry } from '../utils/get-mfe-registry';
2
3
  import { defineEntry } from './define-entry';
3
4
 
4
5
  export const entry = defineEntry({
@@ -9,8 +10,9 @@ export const entry = defineEntry({
9
10
  },
10
11
  registry: {
11
12
  string: true,
12
- hidden: true,
13
- description: 'Registry url',
13
+ hidden: false,
14
+ description: 'Registry to roll back within',
15
+ defaultDescription: getMfeRegistry(),
14
16
  },
15
17
  tag: {
16
18
  string: true,
@@ -4,6 +4,7 @@ import { defineEntry } from './define-entry';
4
4
  export const entry = defineEntry({
5
5
  options: {
6
6
  dry: { boolean: true, description: 'Dry run mode' },
7
+ registry: { string: true, hidden: true, description: 'Registry url' },
7
8
  releaseVersion: {
8
9
  string: true,
9
10
  demandOption: true,
@@ -1,10 +1,10 @@
1
- import { isCI } from '@servicetitan/install';
2
1
  import { execSync } from 'child_process';
3
2
  import path from 'path';
4
3
  import { getTsConfig, isWebComponent, log, logErrors, readJson } from '../../utils';
5
4
  import { TSConfig } from '../utils';
6
5
  import type { entry } from './registry/upload-sourcemaps';
7
6
  import { Command } from './types';
7
+ import { getUnpkgHost } from './utils';
8
8
 
9
9
  interface PackageJson {
10
10
  name: string;
@@ -29,11 +29,7 @@ export class UploadSourcemaps extends Command<typeof entry> {
29
29
 
30
30
  private checkArgs() {
31
31
  if (!process.env.DATADOG_API_KEY) {
32
- const message = 'DATADOG_API_KEY environment variable is not set';
33
- if (!isCI()) {
34
- throw new Error(message);
35
- }
36
- log.warning(`${message}; skipping sourcemaps`);
32
+ log.warning('DATADOG_API_KEY environment variable is not set; skipping sourcemaps');
37
33
  return false;
38
34
  }
39
35
 
@@ -52,7 +48,8 @@ export class UploadSourcemaps extends Command<typeof entry> {
52
48
 
53
49
  private getMinifiedPrefixPath() {
54
50
  const { name } = this.getPackageJson();
55
- const baseUrl = `https://unpkg.servicetitan.com/${name}@${this.args.releaseVersion}`;
51
+ const host = getUnpkgHost(this.args.registry);
52
+ const baseUrl = `${host}/${name}@${this.args.releaseVersion}`;
56
53
  return `${baseUrl}/${this.getBundleDir().replace(/\\/g, '/')}`;
57
54
  }
58
55
 
@@ -0,0 +1,24 @@
1
+ import { PROD_MFE_REGISTRY, getMfeRegistry } from '../get-mfe-registry';
2
+
3
+ describe(`[startup] ${getMfeRegistry.name}`, () => {
4
+ let registry: string | undefined;
5
+
6
+ beforeEach(() => {
7
+ jest.clearAllMocks();
8
+ registry = undefined;
9
+ });
10
+
11
+ const subject = () => getMfeRegistry(registry);
12
+
13
+ test('returns the production registry', () => {
14
+ expect(subject()).toBe(PROD_MFE_REGISTRY);
15
+ });
16
+
17
+ describe('with an explicit registry', () => {
18
+ beforeEach(() => (registry = 'https://foo.com'));
19
+
20
+ test('returns the specified registry', () => {
21
+ expect(subject()).toBe(registry);
22
+ });
23
+ });
24
+ });
@@ -0,0 +1,32 @@
1
+ import { DEV_MFE_REGISTRY, getMfeRegistry, PROD_MFE_REGISTRY } from '../get-mfe-registry';
2
+ import { getUnpkgHost } from '../get-unpkg-host';
3
+
4
+ jest.mock('../get-mfe-registry', () => ({
5
+ ...jest.requireActual('../get-mfe-registry'),
6
+ getMfeRegistry: jest.fn(),
7
+ }));
8
+
9
+ describe(`[startup] ${getUnpkgHost.name}`, () => {
10
+ const registry = 'http://foo.com';
11
+
12
+ const subject = () => getUnpkgHost(registry);
13
+
14
+ test('fetches MFE registry', () => {
15
+ subject();
16
+
17
+ expect(getMfeRegistry).toHaveBeenCalledWith(registry);
18
+ });
19
+
20
+ describe.each([
21
+ [DEV_MFE_REGISTRY, 'https://unpkg.st.dev'],
22
+ [PROD_MFE_REGISTRY, 'https://unpkg.servicetitan.com'],
23
+ ])('when the MFE registry is %s', (mfeRegistry, expected) => {
24
+ beforeEach(() => {
25
+ jest.mocked(getMfeRegistry).mockImplementation(() => mfeRegistry);
26
+ });
27
+
28
+ test(`returns ${expected}`, () => {
29
+ expect(subject()).toBe(expected);
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,122 @@
1
+ import { npmWhoAmI } from '@servicetitan/install';
2
+ import { createInterface } from 'readline/promises';
3
+ import { isTTY, runCommand } from '../../../utils';
4
+ import { DEV_MFE_REGISTRY, PROD_MFE_REGISTRY } from '../get-mfe-registry';
5
+ import { login } from '../login';
6
+
7
+ jest.mock('@servicetitan/install');
8
+ jest.mock('readline/promises', () => ({ createInterface: jest.fn() }));
9
+ jest.mock('../../../utils', () => ({
10
+ ...jest.requireActual('../../../utils'),
11
+ isTTY: jest.fn(),
12
+ runCommand: jest.fn(),
13
+ }));
14
+
15
+ describe(`[startup] ${login.name}`, () => {
16
+ const readline = { question: jest.fn(), close: jest.fn() };
17
+ let answer: string;
18
+ let registry: string;
19
+ let user: string | undefined;
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ answer = 'Y';
24
+ registry = PROD_MFE_REGISTRY;
25
+ user = undefined;
26
+ jest.mocked(readline.question).mockImplementation(() => Promise.resolve(answer));
27
+ jest.mocked(createInterface).mockReturnValue(readline as any);
28
+ jest.mocked(npmWhoAmI).mockImplementation(() => user);
29
+ jest.mocked(isTTY).mockReturnValue(true);
30
+ });
31
+
32
+ const subject = () => login(registry);
33
+
34
+ test('prompts to authorize the machine', async () => {
35
+ await subject();
36
+
37
+ expect(readline.question).toHaveBeenCalledWith(
38
+ expect.stringMatching(/machine is not authorized.*Authorize\? \[Y\/n\]/)
39
+ );
40
+ });
41
+
42
+ test('closes the prompt', async () => {
43
+ await subject();
44
+
45
+ expect(readline.close).toHaveBeenCalled();
46
+ });
47
+
48
+ function itAuthorizesTheMachine() {
49
+ test('authorizes the machine against the registry', async () => {
50
+ await subject();
51
+
52
+ expect(runCommand).toHaveBeenCalledWith(
53
+ `npx --yes verdaccio-okta-oauth@latest --registry=${registry}`,
54
+ { quiet: true }
55
+ );
56
+ });
57
+ }
58
+
59
+ itAuthorizesTheMachine();
60
+
61
+ describe('when user does not answer', () => {
62
+ beforeEach(() => (answer = ''));
63
+
64
+ itAuthorizesTheMachine();
65
+ });
66
+
67
+ describe('when user declines', () => {
68
+ beforeEach(() => (answer = 'n'));
69
+
70
+ test('throws error', async () => {
71
+ await expect(subject()).rejects.toThrow(/Not authorized/);
72
+ });
73
+ });
74
+
75
+ describe('when terminal is not interactive', () => {
76
+ beforeEach(() => jest.mocked(isTTY).mockReturnValue(false));
77
+
78
+ test('throws error', async () => {
79
+ await expect(subject()).rejects.toThrow(/machine is not authorized/);
80
+ });
81
+ });
82
+
83
+ function itSkipsPrompt() {
84
+ test('skips the prompt', async () => {
85
+ await subject();
86
+
87
+ expect(readline.question).not.toHaveBeenCalled();
88
+ });
89
+ }
90
+
91
+ describe('when machine is already authorized', () => {
92
+ beforeEach(() => (user = 'someone'));
93
+
94
+ itSkipsPrompt();
95
+ });
96
+
97
+ describe(`when the registry is ${DEV_MFE_REGISTRY}`, () => {
98
+ beforeEach(() => (registry = DEV_MFE_REGISTRY));
99
+
100
+ itAuthorizesTheMachine();
101
+ });
102
+
103
+ describe('with an unrecognized registry', () => {
104
+ beforeEach(() => (registry = 'https://foo.com'));
105
+
106
+ test('throws error', async () => {
107
+ await expect(subject()).rejects.toThrow(/is not recognized/);
108
+ });
109
+
110
+ test('does not attempt oauth', async () => {
111
+ await expect(subject()).rejects.toThrow();
112
+
113
+ expect(runCommand).not.toHaveBeenCalled();
114
+ });
115
+
116
+ describe('when machine is already authorized', () => {
117
+ beforeEach(() => (user = 'someone'));
118
+
119
+ itSkipsPrompt();
120
+ });
121
+ });
122
+ });
@@ -1,40 +1,42 @@
1
1
  import { runCommandOutput } from '../../../utils';
2
+ import { getUnpkgHost } from '../get-unpkg-host';
2
3
  import { purgeCache } from '../purge-cache';
3
4
 
5
+ jest.mock('@servicetitan/install', () => ({ isCI: jest.fn() }));
4
6
  jest.mock('../../../utils', () => ({
5
7
  ...jest.requireActual('../../../utils'),
6
8
  runCommandOutput: jest.fn(),
7
9
  }));
10
+ jest.mock('../get-unpkg-host', () => ({
11
+ getUnpkgHost: jest.fn(),
12
+ }));
8
13
 
9
14
  describe(purgeCache.name, () => {
10
- let args: Parameters<typeof purgeCache>[0];
15
+ const unpkgHost = 'https://foo.com';
11
16
  const packageName = '@servicetitan/foo';
17
+ const registry = 'https://bar.com';
12
18
  const tag = 'prod';
19
+ let args: Parameters<typeof purgeCache>[0];
13
20
 
14
21
  beforeEach(() => {
15
22
  jest.clearAllMocks();
16
- args = { packageName, tag };
23
+ jest.mocked(getUnpkgHost).mockReturnValue(unpkgHost);
24
+ args = { packageName, registry, tag };
17
25
  });
18
26
 
19
27
  const subject = () => purgeCache(args);
20
28
 
21
- test('runs mfe-purge-cache with host "https://unpkg.servicetitan.com", package name and tag', () => {
29
+ test('fetches host for registry', () => {
22
30
  subject();
23
31
 
24
- expect(runCommandOutput).toHaveBeenCalledWith(
25
- `npx startup mfe-purge-cache --host https://unpkg.servicetitan.com --package-name ${packageName} --tag ${tag}`
26
- );
32
+ expect(getUnpkgHost).toHaveBeenCalledWith(registry);
27
33
  });
28
34
 
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();
35
+ test('runs mfe-purge-cache', () => {
36
+ subject();
34
37
 
35
- expect(runCommandOutput).toHaveBeenCalledWith(
36
- expect.stringContaining(`--host https://unpkg.st.dev`)
37
- );
38
- });
38
+ expect(runCommandOutput).toHaveBeenCalledWith(
39
+ `npx startup mfe-purge-cache --host ${unpkgHost} --package-name ${packageName} --tag ${tag}`
40
+ );
39
41
  });
40
42
  });
@@ -0,0 +1,6 @@
1
+ export const PROD_MFE_REGISTRY = 'https://verdaccio.servicetitan.com';
2
+ export const DEV_MFE_REGISTRY = 'https://verdaccio.st.dev';
3
+
4
+ export function getMfeRegistry(registry?: string): string {
5
+ return registry ?? PROD_MFE_REGISTRY;
6
+ }
@@ -0,0 +1,8 @@
1
+ import { DEV_MFE_REGISTRY, getMfeRegistry } from './get-mfe-registry';
2
+
3
+ const PROD_UNPKG_HOST = 'https://unpkg.servicetitan.com';
4
+ const DEV_UNPKG_HOST = 'https://unpkg.st.dev';
5
+
6
+ export function getUnpkgHost(registry?: string): string {
7
+ return getMfeRegistry(registry) === DEV_MFE_REGISTRY ? DEV_UNPKG_HOST : PROD_UNPKG_HOST;
8
+ }
@@ -1,3 +1,5 @@
1
1
  export * from './build-rollback-tag';
2
- export * from './constants';
2
+ export * from './get-mfe-registry';
3
+ export * from './get-unpkg-host';
4
+ export * from './login';
3
5
  export * from './purge-cache';
@@ -0,0 +1,38 @@
1
+ import { npmWhoAmI } from '@servicetitan/install';
2
+ import chalk from 'chalk';
3
+ import readline from 'readline/promises';
4
+ import { isTTY, runCommand } from '../../utils';
5
+ import { DEV_MFE_REGISTRY, PROD_MFE_REGISTRY } from './get-mfe-registry';
6
+
7
+ const OKTA_REGISTRIES: string[] = [PROD_MFE_REGISTRY, DEV_MFE_REGISTRY];
8
+
9
+ export async function login(registry: string) {
10
+ if (npmWhoAmI({ registry }) !== undefined) {
11
+ return;
12
+ }
13
+
14
+ if (!OKTA_REGISTRIES.includes(registry)) {
15
+ throw new Error(`Registry ${registry} is not recognized. Please log in manually.`);
16
+ }
17
+
18
+ const message = `This machine is not authorized to access ${registry}.`;
19
+ if (!isTTY()) {
20
+ throw new Error(message);
21
+ }
22
+
23
+ const answer = await ask(chalk.bold.cyan(`${message} Authorize? [Y/n] `));
24
+ if (answer && answer !== 'y') {
25
+ throw new Error('Not authorized');
26
+ }
27
+
28
+ await runCommand(`npx --yes verdaccio-okta-oauth@latest --registry=${registry}`, {
29
+ quiet: true,
30
+ });
31
+ }
32
+
33
+ async function ask(question: string) {
34
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
35
+ const answer = await rl.question(question);
36
+ rl.close();
37
+ return answer.trim().toLowerCase();
38
+ }
@@ -1,12 +1,9 @@
1
1
  import { runCommandOutput } from '../../utils';
2
-
3
- const DEV_MFE_REGISTRY = 'https://verdaccio.st.dev';
4
- const DEFAULT_UNPKG_HOST = 'https://unpkg.servicetitan.com';
5
- const DEV_UNPKG_HOST = 'https://unpkg.st.dev';
2
+ import { getUnpkgHost } from './get-unpkg-host';
6
3
 
7
4
  export function purgeCache(options: { packageName: string; registry?: string; tag: string }) {
8
5
  const { packageName, registry, tag } = options;
9
- const host = registry === DEV_MFE_REGISTRY ? DEV_UNPKG_HOST : DEFAULT_UNPKG_HOST;
6
+ const host = getUnpkgHost(registry);
10
7
  runCommandOutput(
11
8
  `npx startup mfe-purge-cache --host ${host} --package-name ${packageName} --tag ${tag}`
12
9
  );
package/src/cli/index.ts CHANGED
@@ -40,7 +40,8 @@ function addCommand(name: CommandName, y: typeof yargs) {
40
40
  const { description, options: { _, ...options } = {} } = entry;
41
41
  const nameWithArgs = name + (_?.description ? ` ${_.description}` : '');
42
42
  const groupedOptions = Object.entries(options).reduce((result, [key, value]) => {
43
- result[kebabCase(key)] = { group: 'Command Options:', ...value };
43
+ const suffix = /\W+$/.exec(key)?.[0] ?? ''; // fix for kebabCase dropping `\u00A0` suffixes
44
+ result[kebabCase(key) + suffix] = { group: 'Command Options:', ...value };
44
45
  return result;
45
46
  }, {} as CommandOptions);
46
47
  const handler = (argv: any) => handleCommand(name, argv);
@@ -0,0 +1,25 @@
1
+ import { buildEscapedRegExp } from '../build-escaped-reg-exp';
2
+
3
+ describe('buildEscapedRegExp', () => {
4
+ test.each(['react', '@servicetitan/startup-utils', 'react-refresh/runtime'])(
5
+ 'matches %s exactly',
6
+ specifier => {
7
+ expect(buildEscapedRegExp(specifier).test(specifier)).toBe(true);
8
+ }
9
+ );
10
+
11
+ test.each([
12
+ ['lodash.throttle', 'lodashxthrottle'],
13
+ ['lodash.throttle', 'lodash.throttle/extra'],
14
+ ['react', 'react-dom'],
15
+ ['react', 'preact'],
16
+ ])('built from %s does not match %s', (specifier, other) => {
17
+ expect(buildEscapedRegExp(specifier).test(other)).toBe(false);
18
+ });
19
+
20
+ /* Unescaped, a backslash would turn the next character into an escape sequence. */
21
+ test('escapes backslashes rather than emitting them raw', () => {
22
+ expect(buildEscapedRegExp('we\\d').test('we\\d')).toBe(true);
23
+ expect(buildEscapedRegExp('we\\d').test('we5')).toBe(false);
24
+ });
25
+ });
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Dots are the only regex-special character valid in a package name; the
3
+ * backslash is escaped because it is the escape character itself.
4
+ */
5
+ export const buildEscapedRegExp = (specifier: string) =>
6
+ new RegExp(`^${specifier.replace(/[\\.]/g, '\\$&')}$`);
@@ -102,7 +102,7 @@ describe(createBaseConfig.name, () => {
102
102
  context = {
103
103
  build: { isProduction: false },
104
104
  options: {},
105
- package: { isWebComponent: false, destination: 'dist/bundle/full' },
105
+ package: { isWebComponent: false, destination: 'dist/bundle/full', source: 'src' },
106
106
  bundlerConfig: {
107
107
  omitDefault: [],
108
108
  minify: {},
@@ -0,0 +1,85 @@
1
+ import { createRequire } from 'module';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { buildEscapedRegExp } from '../../build-escaped-reg-exp';
6
+ import { ViteBuildContext } from '../../types';
7
+ import { createResolveConfig } from '../create-resolve-config';
8
+
9
+ jest.mock('module', () => ({
10
+ ...jest.requireActual('module'),
11
+ createRequire: jest.fn(() => ({ resolve: jest.fn() })),
12
+ }));
13
+
14
+ describe(`[startup] ${createResolveConfig.name}`, () => {
15
+ let startupRequire: { resolve: (specifier: string) => string };
16
+ let context: Partial<ViteBuildContext>;
17
+ let specifiers: string[];
18
+
19
+ beforeAll(() => {
20
+ startupRequire = jest.mocked(createRequire).mock.results[0].value;
21
+ });
22
+
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ jest.mocked(startupRequire.resolve).mockImplementation(() => {
26
+ throw new Error('Module not found');
27
+ });
28
+ const source = fs.mkdtempSync(path.join(os.tmpdir(), 'create-resolve-config-'));
29
+ context = { package: { source } as any };
30
+ specifiers = ['a', '@b/c'];
31
+ });
32
+
33
+ afterEach(() => fs.rmSync(context.package!.source, { recursive: true, force: true }));
34
+
35
+ function mockNodeModules(root: string, modules: string[]) {
36
+ modules.forEach(name => {
37
+ const dir = path.join(root, 'node_modules', ...name.split('/'));
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ fs.writeFileSync(path.join(dir, 'index.js'), '');
40
+ });
41
+ }
42
+
43
+ const subject = () =>
44
+ createResolveConfig(context as any, specifiers) as {
45
+ alias: { find: RegExp; replacement: string }[];
46
+ };
47
+
48
+ test('throws error', () => {
49
+ expect(() => subject()).toThrow(/cannot resolve/i);
50
+ });
51
+
52
+ describe('when specifier is resolvable', () => {
53
+ beforeEach(() => mockNodeModules(context.package!.source, specifiers));
54
+
55
+ test('does nothing', () => {
56
+ expect(subject().alias).toEqual([]);
57
+ });
58
+ });
59
+
60
+ describe('when specifier is resolvable from startup', () => {
61
+ beforeEach(() => {
62
+ jest.mocked(startupRequire.resolve).mockImplementation(
63
+ (specifier: string) => `startup/${specifier}`
64
+ );
65
+ });
66
+
67
+ test('emits alias', () => {
68
+ const { alias } = subject();
69
+
70
+ specifiers.forEach(specifier => {
71
+ expect(alias.find(({ find }) => find.test(specifier))?.replacement).toEqual(
72
+ `startup/${specifier}`
73
+ );
74
+ });
75
+ });
76
+
77
+ test('alias matches whole specifier', () => {
78
+ const { alias } = subject();
79
+
80
+ specifiers.forEach((specifier, i) =>
81
+ expect(alias[i].find).toEqual(buildEscapedRegExp(specifier))
82
+ );
83
+ });
84
+ });
85
+ });