@shopify/cli-kit 4.9.5 → 5.0.1

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 (127) hide show
  1. package/dist/cli/api/graphql/admin/generated/metafield_definitions_by_owner_type.d.ts +5 -0
  2. package/dist/cli/api/graphql/admin/generated/metafield_definitions_by_owner_type.js +22 -0
  3. package/dist/cli/api/graphql/admin/generated/metafield_definitions_by_owner_type.js.map +1 -1
  4. package/dist/private/node/api/graphql.d.ts +2 -1
  5. package/dist/private/node/api/graphql.js +12 -1
  6. package/dist/private/node/api/graphql.js.map +1 -1
  7. package/dist/private/node/api.d.ts +2 -0
  8. package/dist/private/node/api.js +49 -22
  9. package/dist/private/node/api.js.map +1 -1
  10. package/dist/private/node/command-json-schema.js +1 -0
  11. package/dist/private/node/command-json-schema.js.map +1 -1
  12. package/dist/private/node/constants.d.ts +1 -0
  13. package/dist/private/node/constants.js +1 -0
  14. package/dist/private/node/constants.js.map +1 -1
  15. package/dist/private/node/dev-instance-output.d.ts +47 -0
  16. package/dist/private/node/dev-instance-output.js +143 -0
  17. package/dist/private/node/dev-instance-output.js.map +1 -0
  18. package/dist/private/node/json-error.d.ts +1 -15
  19. package/dist/private/node/json-error.js +4 -98
  20. package/dist/private/node/json-error.js.map +1 -1
  21. package/dist/private/node/output-context.d.ts +8 -0
  22. package/dist/private/node/output-context.js +10 -0
  23. package/dist/private/node/output-context.js.map +1 -0
  24. package/dist/private/node/output.test.js +4 -4
  25. package/dist/private/node/output.test.js.map +1 -1
  26. package/dist/private/node/session/automation-token.d.ts +50 -0
  27. package/dist/private/node/session/automation-token.js +77 -0
  28. package/dist/private/node/session/automation-token.js.map +1 -0
  29. package/dist/private/node/session/exchange.d.ts +5 -1
  30. package/dist/private/node/session/exchange.js +97 -7
  31. package/dist/private/node/session/exchange.js.map +1 -1
  32. package/dist/private/node/session/permanent-store-domain.d.ts +21 -0
  33. package/dist/private/node/session/permanent-store-domain.js +72 -0
  34. package/dist/private/node/session/permanent-store-domain.js.map +1 -0
  35. package/dist/private/node/session.d.ts +3 -2
  36. package/dist/private/node/session.js +12 -3
  37. package/dist/private/node/session.js.map +1 -1
  38. package/dist/private/node/themes/trust-challenge.d.ts +31 -0
  39. package/dist/private/node/themes/trust-challenge.js +143 -0
  40. package/dist/private/node/themes/trust-challenge.js.map +1 -0
  41. package/dist/private/node/ui/alert.js +1 -1
  42. package/dist/private/node/ui/alert.js.map +1 -1
  43. package/dist/private/node/ui/components/ConcurrentOutput.d.ts +2 -7
  44. package/dist/private/node/ui/components/ConcurrentOutput.js +4 -8
  45. package/dist/private/node/ui/components/ConcurrentOutput.js.map +1 -1
  46. package/dist/private/node/ui/components/LoadingBar.js +9 -15
  47. package/dist/private/node/ui/components/LoadingBar.js.map +1 -1
  48. package/dist/private/node/ui/components/LoadingBar.test.js +45 -107
  49. package/dist/private/node/ui/components/LoadingBar.test.js.map +1 -1
  50. package/dist/private/node/ui/components/LoadingIndicator.d.ts +7 -0
  51. package/dist/private/node/ui/components/LoadingIndicator.js +22 -0
  52. package/dist/private/node/ui/components/LoadingIndicator.js.map +1 -0
  53. package/dist/private/node/ui/components/Table/Table.d.ts +8 -0
  54. package/dist/private/node/ui/components/Table/Table.js +18 -2
  55. package/dist/private/node/ui/components/Table/Table.js.map +1 -1
  56. package/dist/public/common/version.d.ts +1 -1
  57. package/dist/public/common/version.js +1 -1
  58. package/dist/public/common/version.js.map +1 -1
  59. package/dist/public/node/api/admin.js +13 -0
  60. package/dist/public/node/api/admin.js.map +1 -1
  61. package/dist/public/node/api/graphql.js +38 -7
  62. package/dist/public/node/api/graphql.js.map +1 -1
  63. package/dist/public/node/archiver.js +13 -6
  64. package/dist/public/node/archiver.js.map +1 -1
  65. package/dist/public/node/base-command.d.ts +6 -0
  66. package/dist/public/node/base-command.js +97 -16
  67. package/dist/public/node/base-command.js.map +1 -1
  68. package/dist/public/node/dev-instance.d.ts +138 -0
  69. package/dist/public/node/dev-instance.js +365 -0
  70. package/dist/public/node/dev-instance.js.map +1 -0
  71. package/dist/public/node/environment.d.ts +6 -3
  72. package/dist/public/node/environment.js +11 -4
  73. package/dist/public/node/environment.js.map +1 -1
  74. package/dist/public/node/error/index.js +2 -1
  75. package/dist/public/node/error/index.js.map +1 -1
  76. package/dist/public/node/error/schema.d.ts +24 -24
  77. package/dist/public/node/error/serialization.d.ts +8 -0
  78. package/dist/public/node/error/serialization.js +103 -0
  79. package/dist/public/node/error/serialization.js.map +1 -0
  80. package/dist/public/node/graphiql/server.js +18 -2
  81. package/dist/public/node/graphiql/server.js.map +1 -1
  82. package/dist/public/node/is-global.js +2 -1
  83. package/dist/public/node/is-global.js.map +1 -1
  84. package/dist/public/node/notifications-system.d.ts +6 -6
  85. package/dist/public/node/path.d.ts +19 -3
  86. package/dist/public/node/path.js +21 -3
  87. package/dist/public/node/path.js.map +1 -1
  88. package/dist/public/node/session.d.ts +38 -3
  89. package/dist/public/node/session.js +56 -9
  90. package/dist/public/node/session.js.map +1 -1
  91. package/dist/public/node/system.d.ts +4 -0
  92. package/dist/public/node/system.js +39 -9
  93. package/dist/public/node/system.js.map +1 -1
  94. package/dist/public/node/themes/api.d.ts +9 -0
  95. package/dist/public/node/themes/api.js +50 -22
  96. package/dist/public/node/themes/api.js.map +1 -1
  97. package/dist/public/node/themes/theme-manager.d.ts +8 -1
  98. package/dist/public/node/themes/theme-manager.js +1 -1
  99. package/dist/public/node/themes/theme-manager.js.map +1 -1
  100. package/dist/public/node/tree-kill.js +12 -2
  101. package/dist/public/node/tree-kill.js.map +1 -1
  102. package/dist/public/node/ui/components.d.ts +1 -0
  103. package/dist/public/node/ui/components.js +1 -0
  104. package/dist/public/node/ui/components.js.map +1 -1
  105. package/dist/public/node/ui.d.ts +24 -4
  106. package/dist/public/node/ui.js +54 -21
  107. package/dist/public/node/ui.js.map +1 -1
  108. package/dist/public/node/upgrade/output.d.ts +17 -0
  109. package/dist/public/node/upgrade/output.js +51 -0
  110. package/dist/public/node/upgrade/output.js.map +1 -0
  111. package/dist/public/node/upgrade/result.d.ts +8 -0
  112. package/dist/public/node/upgrade/result.js +21 -0
  113. package/dist/public/node/upgrade/result.js.map +1 -0
  114. package/dist/public/node/upgrade/types.d.ts +61 -0
  115. package/dist/public/node/upgrade/types.js +46 -0
  116. package/dist/public/node/upgrade/types.js.map +1 -0
  117. package/dist/public/node/upgrade.d.ts +8 -0
  118. package/dist/public/node/upgrade.js +57 -22
  119. package/dist/public/node/upgrade.js.map +1 -1
  120. package/dist/tsconfig.tsbuildinfo +1 -1
  121. package/package.json +3 -3
  122. package/dist/private/node/ui/components/TextAnimation.d.ts +0 -10
  123. package/dist/private/node/ui/components/TextAnimation.js +0 -56
  124. package/dist/private/node/ui/components/TextAnimation.js.map +0 -1
  125. package/dist/private/node/ui/components/TextAnimation.test.d.ts +0 -1
  126. package/dist/private/node/ui/components/TextAnimation.test.js +0 -73
  127. package/dist/private/node/ui/components/TextAnimation.test.js.map +0 -1
@@ -0,0 +1,143 @@
1
+ import { extractGraphQLErrors } from '../api/graphql.js';
2
+ import { commandEventOutputMode } from '../command-event-context.js';
3
+ import { recordEvent } from '../../../public/node/analytics.js';
4
+ import { adminFqdn } from '../../../public/node/context/fqdn.js';
5
+ import { AbortError } from '../../../public/node/error.js';
6
+ import { outputDebug } from '../../../public/node/output.js';
7
+ import { isCI, openURL } from '../../../public/node/system.js';
8
+ import { isTTY, keypress, renderInfo, renderSuccess } from '../../../public/node/ui.js';
9
+ const TRUST_CHALLENGE_ERROR_CODE = 'CHALLENGE_REQUIRED';
10
+ // Enter or Space. `theme dev` binds other single keys on the same stdin to its own shortcuts.
11
+ const CONTINUE_KEYS = ['\r', '\n', ' '];
12
+ /**
13
+ * The abort that ends a Trust Battery challenge: the session cannot answer it, or
14
+ * the request was challenged again after the verification.
15
+ */
16
+ export class TrustChallengeError extends AbortError {
17
+ }
18
+ let pendingChallenge;
19
+ let completedChallenges = 0;
20
+ /**
21
+ * Runs a theme Admin API request and, when Core answers with a Trust Battery
22
+ * challenge, waits for the user to complete it in the browser before retrying once.
23
+ *
24
+ * A request that starts while another request's challenge is still being completed
25
+ * waits for that challenge first, so it is sent once instead of being challenged
26
+ * itself and then retried. A request that was already in flight when a challenge
27
+ * was completed retries without a second prompt, unless another prompt is open:
28
+ * then it waits for that prompt too.
29
+ *
30
+ * @param request - The request to run and possibly retry.
31
+ * @returns The result of the request.
32
+ */
33
+ export async function withTrustChallengeRetry(request) {
34
+ if (pendingChallenge)
35
+ await pendingChallenge;
36
+ const completedChallengesAtStart = completedChallenges;
37
+ const attempt = await attemptRequest(request);
38
+ if (!attempt.challenged)
39
+ return attempt.result;
40
+ const verifiedSinceStart = completedChallenges > completedChallengesAtStart;
41
+ if (pendingChallenge || !verifiedSinceStart)
42
+ await completeTrustChallenge(attempt.challengeUrl);
43
+ const retry = await attemptRequest(request);
44
+ if (!retry.challenged)
45
+ return retry.result;
46
+ recordEvent('theme-api:trust-challenge:retry-challenged');
47
+ throw new TrustChallengeError('Shopify still needs to verify your identity before it can continue.', undefined, [
48
+ ['Complete the verification at', { link: { url: retry.challengeUrl } }, 'then run the command again.'],
49
+ ]);
50
+ }
51
+ /**
52
+ * Runs the request once. A Trust Battery challenge becomes a result with the challenge URL;
53
+ * any other error is thrown.
54
+ */
55
+ async function attemptRequest(request) {
56
+ try {
57
+ return { challenged: false, result: await request() };
58
+ }
59
+ catch (error) {
60
+ const challengeUrl = await trustChallengeUrl(error);
61
+ if (!challengeUrl)
62
+ throw error;
63
+ return { challenged: true, challengeUrl };
64
+ }
65
+ }
66
+ /**
67
+ * Extracts the challenge URL from a Trust Battery `CHALLENGE_REQUIRED` GraphQL error.
68
+ *
69
+ * Only URLs on the Admin host are trusted. Anything else is not treated as a
70
+ * challenge and the original error surfaces through normal error handling.
71
+ *
72
+ * @param error - The error thrown by an Admin API request.
73
+ * @returns The challenge URL, or undefined when the error is not a trust challenge.
74
+ */
75
+ export async function trustChallengeUrl(error) {
76
+ const challengeError = extractGraphQLErrors(error).find((graphQLError) => graphQLError.extensions?.code === TRUST_CHALLENGE_ERROR_CODE);
77
+ const redirectTo = challengeError?.extensions?.redirect_to;
78
+ if (typeof redirectTo !== 'string')
79
+ return undefined;
80
+ if (!(await isAdminUrl(redirectTo))) {
81
+ outputDebug(`Ignoring trust challenge with an unexpected URL host: ${redirectTo}`);
82
+ return undefined;
83
+ }
84
+ return redirectTo;
85
+ }
86
+ async function isAdminUrl(url) {
87
+ try {
88
+ const parsedUrl = new URL(url);
89
+ return parsedUrl.protocol === 'https:' && parsedUrl.hostname === (await adminFqdn());
90
+ // eslint-disable-next-line no-catch-all/no-catch-all
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ /**
97
+ * Concurrent theme requests (for example the parallel batches of `theme push`)
98
+ * all receive the same challenge. Share one prompt so the browser opens once and
99
+ * every waiting request retries after the same verification.
100
+ */
101
+ async function completeTrustChallenge(challengeUrl) {
102
+ pendingChallenge ??= promptForTrustChallenge(challengeUrl)
103
+ .then(() => {
104
+ completedChallenges += 1;
105
+ })
106
+ .finally(() => {
107
+ pendingChallenge = undefined;
108
+ });
109
+ return pendingChallenge;
110
+ }
111
+ async function promptForTrustChallenge(challengeUrl) {
112
+ recordEvent('theme-api:trust-challenge:required');
113
+ if (isCI() || !isTTY() || commandEventOutputMode() === 'json') {
114
+ recordEvent('theme-api:trust-challenge:non-interactive');
115
+ throw new TrustChallengeError('Shopify needs to verify your identity before it can continue.', undefined, [
116
+ [
117
+ 'Open',
118
+ { link: { url: challengeUrl } },
119
+ "in a browser where you're logged in to Shopify with the same account you use in Shopify CLI, complete the verification, then run the command again.",
120
+ ],
121
+ ]);
122
+ }
123
+ const opened = await openURL(challengeUrl);
124
+ recordEvent(opened ? 'theme-api:trust-challenge:browser-opened' : 'theme-api:trust-challenge:browser-not-opened');
125
+ // Everything the user needs is inside the banner: long-running commands such as
126
+ // `theme push` redraw an Ink progress bar that erases any plain line written after it.
127
+ renderInfo({
128
+ headline: 'Verify your identity to continue.',
129
+ body: opened
130
+ ? 'A verification page opened in your browser.'
131
+ : 'Shopify needs to verify your identity before it can continue.',
132
+ link: opened ? undefined : { label: 'Open the verification page', url: challengeUrl },
133
+ nextSteps: [
134
+ ['Complete the verification in your browser, logged in with the same account you use in Shopify CLI.'],
135
+ ['Press', { userInput: 'Enter' }, 'here once you have completed it.'],
136
+ ],
137
+ orderedNextSteps: true,
138
+ });
139
+ await keypress(process.stdin, undefined, { keys: CONTINUE_KEYS });
140
+ recordEvent('theme-api:trust-challenge:acknowledged');
141
+ renderSuccess({ headline: 'Continuing.' });
142
+ }
143
+ //# sourceMappingURL=trust-challenge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust-challenge.js","sourceRoot":"","sources":["../../../../src/private/node/themes/trust-challenge.ts"],"names":[],"mappings":"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","sourcesContent":["import {extractGraphQLErrors} from '../api/graphql.js'\nimport {commandEventOutputMode} from '../command-event-context.js'\nimport {recordEvent} from '../../../public/node/analytics.js'\nimport {adminFqdn} from '../../../public/node/context/fqdn.js'\nimport {AbortError} from '../../../public/node/error.js'\nimport {outputDebug} from '../../../public/node/output.js'\nimport {isCI, openURL} from '../../../public/node/system.js'\nimport {isTTY, keypress, renderInfo, renderSuccess} from '../../../public/node/ui.js'\n\nconst TRUST_CHALLENGE_ERROR_CODE = 'CHALLENGE_REQUIRED'\n// Enter or Space. `theme dev` binds other single keys on the same stdin to its own shortcuts.\nconst CONTINUE_KEYS = ['\\r', '\\n', ' ']\n\n/**\n * The abort that ends a Trust Battery challenge: the session cannot answer it, or\n * the request was challenged again after the verification.\n */\nexport class TrustChallengeError extends AbortError {}\n\nlet pendingChallenge: Promise<void> | undefined\nlet completedChallenges = 0\n\n/**\n * Runs a theme Admin API request and, when Core answers with a Trust Battery\n * challenge, waits for the user to complete it in the browser before retrying once.\n *\n * A request that starts while another request's challenge is still being completed\n * waits for that challenge first, so it is sent once instead of being challenged\n * itself and then retried. A request that was already in flight when a challenge\n * was completed retries without a second prompt, unless another prompt is open:\n * then it waits for that prompt too.\n *\n * @param request - The request to run and possibly retry.\n * @returns The result of the request.\n */\nexport async function withTrustChallengeRetry<T>(request: () => Promise<T>): Promise<T> {\n if (pendingChallenge) await pendingChallenge\n const completedChallengesAtStart = completedChallenges\n\n const attempt = await attemptRequest(request)\n if (!attempt.challenged) return attempt.result\n\n const verifiedSinceStart = completedChallenges > completedChallengesAtStart\n if (pendingChallenge || !verifiedSinceStart) await completeTrustChallenge(attempt.challengeUrl)\n\n const retry = await attemptRequest(request)\n if (!retry.challenged) return retry.result\n\n recordEvent('theme-api:trust-challenge:retry-challenged')\n throw new TrustChallengeError('Shopify still needs to verify your identity before it can continue.', undefined, [\n ['Complete the verification at', {link: {url: retry.challengeUrl}}, 'then run the command again.'],\n ])\n}\n\ntype RequestAttempt<T> = {challenged: false; result: T} | {challenged: true; challengeUrl: string}\n\n/**\n * Runs the request once. A Trust Battery challenge becomes a result with the challenge URL;\n * any other error is thrown.\n */\nasync function attemptRequest<T>(request: () => Promise<T>): Promise<RequestAttempt<T>> {\n try {\n return {challenged: false, result: await request()}\n } catch (error) {\n const challengeUrl = await trustChallengeUrl(error)\n if (!challengeUrl) throw error\n return {challenged: true, challengeUrl}\n }\n}\n\n/**\n * Extracts the challenge URL from a Trust Battery `CHALLENGE_REQUIRED` GraphQL error.\n *\n * Only URLs on the Admin host are trusted. Anything else is not treated as a\n * challenge and the original error surfaces through normal error handling.\n *\n * @param error - The error thrown by an Admin API request.\n * @returns The challenge URL, or undefined when the error is not a trust challenge.\n */\nexport async function trustChallengeUrl(error: unknown): Promise<string | undefined> {\n const challengeError = extractGraphQLErrors(error).find(\n (graphQLError) => graphQLError.extensions?.code === TRUST_CHALLENGE_ERROR_CODE,\n )\n const redirectTo = challengeError?.extensions?.redirect_to\n if (typeof redirectTo !== 'string') return undefined\n\n if (!(await isAdminUrl(redirectTo))) {\n outputDebug(`Ignoring trust challenge with an unexpected URL host: ${redirectTo}`)\n return undefined\n }\n\n return redirectTo\n}\n\nasync function isAdminUrl(url: string): Promise<boolean> {\n try {\n const parsedUrl = new URL(url)\n return parsedUrl.protocol === 'https:' && parsedUrl.hostname === (await adminFqdn())\n // eslint-disable-next-line no-catch-all/no-catch-all\n } catch {\n return false\n }\n}\n\n/**\n * Concurrent theme requests (for example the parallel batches of `theme push`)\n * all receive the same challenge. Share one prompt so the browser opens once and\n * every waiting request retries after the same verification.\n */\nasync function completeTrustChallenge(challengeUrl: string): Promise<void> {\n pendingChallenge ??= promptForTrustChallenge(challengeUrl)\n .then(() => {\n completedChallenges += 1\n })\n .finally(() => {\n pendingChallenge = undefined\n })\n return pendingChallenge\n}\n\nasync function promptForTrustChallenge(challengeUrl: string): Promise<void> {\n recordEvent('theme-api:trust-challenge:required')\n\n if (isCI() || !isTTY() || commandEventOutputMode() === 'json') {\n recordEvent('theme-api:trust-challenge:non-interactive')\n throw new TrustChallengeError('Shopify needs to verify your identity before it can continue.', undefined, [\n [\n 'Open',\n {link: {url: challengeUrl}},\n \"in a browser where you're logged in to Shopify with the same account you use in Shopify CLI, complete the verification, then run the command again.\",\n ],\n ])\n }\n\n const opened = await openURL(challengeUrl)\n recordEvent(opened ? 'theme-api:trust-challenge:browser-opened' : 'theme-api:trust-challenge:browser-not-opened')\n\n // Everything the user needs is inside the banner: long-running commands such as\n // `theme push` redraw an Ink progress bar that erases any plain line written after it.\n renderInfo({\n headline: 'Verify your identity to continue.',\n body: opened\n ? 'A verification page opened in your browser.'\n : 'Shopify needs to verify your identity before it can continue.',\n link: opened ? undefined : {label: 'Open the verification page', url: challengeUrl},\n nextSteps: [\n ['Complete the verification in your browser, logged in with the same account you use in Shopify CLI.'],\n ['Press', {userInput: 'Enter'}, 'here once you have completed it.'],\n ],\n orderedNextSteps: true,\n })\n\n await keypress(process.stdin, undefined, {keys: CONTINUE_KEYS})\n\n recordEvent('theme-api:trust-challenge:acknowledged')\n renderSuccess({headline: 'Continuing.'})\n}\n"]}
@@ -12,7 +12,7 @@ const typeToLogLevel = {
12
12
  };
13
13
  export function alert(options) {
14
14
  const { type, headline, body, nextSteps, reference, link, customSections, orderedNextSteps = false, renderOptions, } = options;
15
- if ((type === 'info' || type === 'warning') && commandEventOutputMode() === 'json') {
15
+ if ((type === 'info' || type === 'warning' || type === 'success') && commandEventOutputMode() === 'json') {
16
16
  const message = alertMessage(options);
17
17
  if (type === 'warning')
18
18
  outputWarn(message);
@@ -1 +1 @@
1
- {"version":3,"file":"alert.js","sourceRoot":"","sources":["../../../../src/private/node/ui/alert.tsx"],"names":[],"mappings":"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","sourcesContent":["import {Alert, AlertProps} from './components/Alert.js'\nimport {tokenItemToJsonString} from './components/token-item.js'\nimport {renderOnce} from '../ui.js'\nimport {commandEventOutputMode} from '../command-event-context.js'\nimport {LogLevel, outputInfo, outputWarn} from '../../../public/node/output.js'\nimport React from 'react'\n\nimport {RenderOptions} from 'ink'\n\nconst typeToLogLevel: Record<AlertProps['type'], LogLevel> = {\n info: 'info',\n warning: 'warn',\n success: 'info',\n error: 'error',\n}\n\nexport interface AlertOptions extends AlertProps {\n renderOptions?: RenderOptions\n}\n\nexport function alert(options: AlertOptions) {\n const {\n type,\n headline,\n body,\n nextSteps,\n reference,\n link,\n customSections,\n orderedNextSteps = false,\n renderOptions,\n } = options\n\n if ((type === 'info' || type === 'warning') && commandEventOutputMode() === 'json') {\n const message = alertMessage(options)\n if (type === 'warning') outputWarn(message)\n else outputInfo(message)\n return message\n }\n\n return renderOnce(\n <Alert\n type={type}\n headline={headline}\n body={body}\n nextSteps={nextSteps}\n reference={reference}\n link={link}\n orderedNextSteps={orderedNextSteps}\n customSections={customSections}\n />,\n {logLevel: typeToLogLevel[type], renderOptions},\n )\n}\n\nfunction alertMessage({headline, body, nextSteps, reference, link, customSections}: AlertProps): string {\n const sections = [\n headline ? tokenItemToJsonString(headline) : undefined,\n body ? tokenItemToJsonString(body) : undefined,\n nextSteps?.length ? `Next steps:\\n${nextSteps.map(tokenItemToJsonString).join('\\n')}` : undefined,\n reference?.length ? `Reference:\\n${reference.map(tokenItemToJsonString).join('\\n')}` : undefined,\n link ? tokenItemToJsonString({link}) : undefined,\n ...(customSections ?? []).map((section) => {\n const content =\n typeof section.body === 'object' && 'tabularData' in section.body\n ? section.body.tabularData.map((row) => row.map(tokenItemToJsonString).join('\\t')).join('\\n')\n : tokenItemToJsonString(section.body)\n return section.title ? `${section.title}:\\n${content}` : content\n }),\n ]\n return sections.filter((section): section is string => section !== undefined).join('\\n\\n')\n}\n"]}
1
+ {"version":3,"file":"alert.js","sourceRoot":"","sources":["../../../../src/private/node/ui/alert.tsx"],"names":[],"mappings":"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","sourcesContent":["import {Alert, AlertProps} from './components/Alert.js'\nimport {tokenItemToJsonString} from './components/token-item.js'\nimport {renderOnce} from '../ui.js'\nimport {commandEventOutputMode} from '../command-event-context.js'\nimport {LogLevel, outputInfo, outputWarn} from '../../../public/node/output.js'\nimport React from 'react'\n\nimport {RenderOptions} from 'ink'\n\nconst typeToLogLevel: Record<AlertProps['type'], LogLevel> = {\n info: 'info',\n warning: 'warn',\n success: 'info',\n error: 'error',\n}\n\nexport interface AlertOptions extends AlertProps {\n renderOptions?: RenderOptions\n}\n\nexport function alert(options: AlertOptions) {\n const {\n type,\n headline,\n body,\n nextSteps,\n reference,\n link,\n customSections,\n orderedNextSteps = false,\n renderOptions,\n } = options\n\n if ((type === 'info' || type === 'warning' || type === 'success') && commandEventOutputMode() === 'json') {\n const message = alertMessage(options)\n if (type === 'warning') outputWarn(message)\n else outputInfo(message)\n return message\n }\n\n return renderOnce(\n <Alert\n type={type}\n headline={headline}\n body={body}\n nextSteps={nextSteps}\n reference={reference}\n link={link}\n orderedNextSteps={orderedNextSteps}\n customSections={customSections}\n />,\n {logLevel: typeToLogLevel[type], renderOptions},\n )\n}\n\nfunction alertMessage({headline, body, nextSteps, reference, link, customSections}: AlertProps): string {\n const sections = [\n headline ? tokenItemToJsonString(headline) : undefined,\n body ? tokenItemToJsonString(body) : undefined,\n nextSteps?.length ? `Next steps:\\n${nextSteps.map(tokenItemToJsonString).join('\\n')}` : undefined,\n reference?.length ? `Reference:\\n${reference.map(tokenItemToJsonString).join('\\n')}` : undefined,\n link ? tokenItemToJsonString({link}) : undefined,\n ...(customSections ?? []).map((section) => {\n const content =\n typeof section.body === 'object' && 'tabularData' in section.body\n ? section.body.tabularData.map((row) => row.map(tokenItemToJsonString).join('\\t')).join('\\n')\n : tokenItemToJsonString(section.body)\n return section.title ? `${section.title}:\\n${content}` : content\n }),\n ]\n return sections.filter((section): section is string => section !== undefined).join('\\n\\n')\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { ConcurrentOutputContext, getConcurrentOutputContext, useConcurrentOutputContext } from '../../output-context.js';
1
2
  import { type OutputProcess } from '../../../../public/node/output.js';
2
3
  import { AbortSignal } from '../../../../public/node/abort.js';
3
4
  import { FunctionComponent } from 'react';
@@ -13,12 +14,6 @@ export interface ConcurrentOutputProps {
13
14
  keepRunningAfterProcessesResolve?: boolean;
14
15
  useAlternativeColorPalette?: boolean;
15
16
  }
16
- interface ConcurrentOutputContext {
17
- outputPrefix?: string;
18
- /** Controls ANSI stripping for terminal output. JSON diagnostics are always unstyled. */
19
- stripAnsi?: boolean;
20
- }
21
- declare function useConcurrentOutputContext<T>(context: ConcurrentOutputContext, callback: () => T): T;
22
17
  /** Runs finite processes concurrently and routes their output through the shared diagnostic context. */
23
18
  export declare function runConcurrentProcessesForJson({ processes, abortSignal, }: Pick<ConcurrentOutputProps, 'processes' | 'abortSignal'>): Promise<void>;
24
19
  /**
@@ -55,4 +50,4 @@ export declare function runConcurrentProcessesForJson({ processes, abortSignal,
55
50
  * ```
56
51
  */
57
52
  declare const ConcurrentOutput: FunctionComponent<ConcurrentOutputProps>;
58
- export { ConcurrentOutput, ConcurrentOutputContext, useConcurrentOutputContext };
53
+ export { ConcurrentOutput, ConcurrentOutputContext, getConcurrentOutputContext, useConcurrentOutputContext };
@@ -1,3 +1,4 @@
1
+ import { getConcurrentOutputContext, useConcurrentOutputContext } from '../../output-context.js';
1
2
  import { outputInfo } from '../../../../public/node/output.js';
2
3
  import { useComplete } from '../../ui.js';
3
4
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
@@ -5,7 +6,6 @@ import { Box, Static, Text } from 'ink';
5
6
  import figures from 'figures';
6
7
  import stripAnsi from 'strip-ansi';
7
8
  import { Writable } from 'stream';
8
- import { AsyncLocalStorage } from 'node:async_hooks';
9
9
  import { StringDecoder } from 'node:string_decoder';
10
10
  function addLeadingZero(number) {
11
11
  if (number < 10) {
@@ -22,10 +22,6 @@ function currentTime() {
22
22
  const seconds = addLeadingZero(currentDateTime.getSeconds());
23
23
  return `${hours}:${minutes}:${seconds}`;
24
24
  }
25
- const outputContextStore = new AsyncLocalStorage();
26
- function useConcurrentOutputContext(context, callback) {
27
- return outputContextStore.run(context, callback);
28
- }
29
25
  /** Runs finite processes concurrently and routes their output through the shared diagnostic context. */
30
26
  export async function runConcurrentProcessesForJson({ processes, abortSignal, }) {
31
27
  await Promise.all(processes.map(async (process) => {
@@ -45,7 +41,7 @@ export async function runConcurrentProcessesForJson({ processes, abortSignal, })
45
41
  next();
46
42
  return;
47
43
  }
48
- const currentPrefix = outputContextStore.getStore()?.outputPrefix ?? process.prefix;
44
+ const currentPrefix = getConcurrentOutputContext()?.outputPrefix ?? process.prefix;
49
45
  if (!hasPendingLine)
50
46
  prefix = currentPrefix;
51
47
  const lines = (pending + decoder.write(chunk)).split(/\r?\n/);
@@ -141,7 +137,7 @@ const ConcurrentOutput = ({ processes, prefixColumnSize, abortSignal, showTimest
141
137
  const writableStream = useCallback((process, prefixes) => {
142
138
  return new Writable({
143
139
  write(chunk, _encoding, next) {
144
- const context = outputContextStore.getStore();
140
+ const context = getConcurrentOutputContext();
145
141
  const prefix = context?.outputPrefix ?? process.prefix;
146
142
  const shouldStripAnsi = context?.stripAnsi ?? true;
147
143
  const log = chunk.toString('utf8').replace(/(\n)$/, '');
@@ -211,5 +207,5 @@ const ConcurrentOutput = ({ processes, prefixColumnSize, abortSignal, showTimest
211
207
  line)))))));
212
208
  }));
213
209
  };
214
- export { ConcurrentOutput, useConcurrentOutputContext };
210
+ export { ConcurrentOutput, getConcurrentOutputContext, useConcurrentOutputContext };
215
211
  //# sourceMappingURL=ConcurrentOutput.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConcurrentOutput.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/ConcurrentOutput.tsx"],"names":[],"mappings":"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","sourcesContent":["import {outputInfo, type OutputProcess} from '../../../../public/node/output.js'\nimport {AbortSignal} from '../../../../public/node/abort.js'\nimport {useComplete} from '../../ui.js'\nimport React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react'\nimport {Box, Static, Text, TextProps} from 'ink'\nimport figures from 'figures'\nimport stripAnsi from 'strip-ansi'\n\nimport {Writable} from 'stream'\nimport {AsyncLocalStorage} from 'node:async_hooks'\nimport {StringDecoder} from 'node:string_decoder'\n\nexport interface ConcurrentOutputProps {\n processes: OutputProcess[]\n prefixColumnSize?: number\n abortSignal: AbortSignal\n showTimestamps?: boolean\n /**\n * Keeps terminal UI running after all processes finish. Defaults to false.\n * In JSON mode, false uses finite progress/diagnostic events; true retains streaming terminal UI.\n */\n keepRunningAfterProcessesResolve?: boolean\n useAlternativeColorPalette?: boolean\n}\n\ninterface Chunk {\n color: TextProps['color']\n prefix: string\n lines: string[]\n}\n\nfunction addLeadingZero(number: number) {\n if (number < 10) {\n return `0${number}`\n } else {\n return number.toString()\n }\n}\n\nfunction currentTime() {\n const currentDateTime = new Date()\n const hours = addLeadingZero(currentDateTime.getHours())\n const minutes = addLeadingZero(currentDateTime.getMinutes())\n const seconds = addLeadingZero(currentDateTime.getSeconds())\n return `${hours}:${minutes}:${seconds}`\n}\n\ninterface ConcurrentOutputContext {\n outputPrefix?: string\n /** Controls ANSI stripping for terminal output. JSON diagnostics are always unstyled. */\n stripAnsi?: boolean\n}\n\nconst outputContextStore = new AsyncLocalStorage<ConcurrentOutputContext>()\n\nfunction useConcurrentOutputContext<T>(context: ConcurrentOutputContext, callback: () => T): T {\n return outputContextStore.run(context, callback)\n}\n\n/** Runs finite processes concurrently and routes their output through the shared diagnostic context. */\nexport async function runConcurrentProcessesForJson({\n processes,\n abortSignal,\n}: Pick<ConcurrentOutputProps, 'processes' | 'abortSignal'>): Promise<void> {\n await Promise.all(\n processes.map(async (process) => {\n const createStream = () => {\n const decoder = new StringDecoder('utf8')\n let pending = ''\n let hasPendingLine = false\n let prefix = process.prefix\n const emitLine = (line: string) => {\n const message = stripAnsi(line)\n if (message.trim().length > 0) outputInfo(`${prefix}: ${message}`)\n }\n const stream = new Writable({\n write(chunk, _encoding, next) {\n if (chunk.length === 0) {\n next()\n return\n }\n const currentPrefix = outputContextStore.getStore()?.outputPrefix ?? process.prefix\n if (!hasPendingLine) prefix = currentPrefix\n const lines = (pending + decoder.write(chunk)).split(/\\r?\\n/)\n pending = lines.pop() ?? ''\n for (const line of lines) {\n emitLine(line)\n prefix = currentPrefix\n }\n hasPendingLine = chunk[chunk.length - 1] !== 10\n next()\n },\n })\n return {\n stream,\n flush: () => {\n emitLine((pending + decoder.end()).replace(/\\r$/, ''))\n pending = ''\n hasPendingLine = false\n },\n }\n }\n const stdout = createStream()\n const stderr = createStream()\n try {\n await process.action(stdout.stream, stderr.stream, abortSignal)\n } finally {\n stdout.flush()\n stderr.flush()\n }\n }),\n )\n}\n\n/**\n * Renders output from concurrent processes to the terminal.\n * Output will be divided in a three column layout\n * with the left column containing the timestamp,\n * the right column containing the output,\n * and the middle column containing the process prefix.\n * Every process will be rendered with a different color, up to 4 colors.\n *\n * For example running `shopify app dev`:\n *\n * ```shell\n * 2022-10-10 13:11:03 | backend | npm\n * 2022-10-10 13:11:03 | backend | WARN ignoring workspace config at ...\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | backend | > shopify-app-template-node@0.1.0 dev\n * 2022-10-10 13:11:03 | backend | > cross-env NODE_ENV=development nodemon backend/index.js --watch ./backend\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | frontend |\n * 2022-10-10 13:11:03 | frontend | > starter-react-frontend-app@0.1.0 dev\n * 2022-10-10 13:11:03 | frontend | > cross-env NODE_ENV=development node vite-server.js\n * 2022-10-10 13:11:03 | frontend |\n * 2022-10-10 13:11:03 | frontend |\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | backend | [nodemon] to restart at any time, enter `rs`\n * 2022-10-10 13:11:03 | backend | [nodemon] watching path(s): backend/\n * 2022-10-10 13:11:03 | backend | [nodemon] watching extensions: js,mjs,json\n * 2022-10-10 13:11:03 | backend | [nodemon] starting `node backend/index.js`\n * 2022-10-10 13:11:03 | backend |\n *\n * ```\n */\nconst ConcurrentOutput: FunctionComponent<ConcurrentOutputProps> = ({\n processes,\n prefixColumnSize,\n abortSignal,\n showTimestamps = true,\n keepRunningAfterProcessesResolve = false,\n useAlternativeColorPalette = false,\n}) => {\n const [processOutput, setProcessOutput] = useState<Chunk[]>([])\n const [completionResult, setCompletionResult] = useState<{error?: Error} | null>(null)\n const complete = useComplete()\n const concurrentColors: TextProps['color'][] = useMemo(\n () =>\n useAlternativeColorPalette\n ? ['#b994c3', '#e69e19', '#d17a73', 'cyan', 'magenta', 'blue']\n : ['yellow', 'cyan', 'magenta', 'green', 'blue'],\n [useAlternativeColorPalette],\n )\n\n const calculatedPrefixColumnSize = useMemo(() => {\n const maxColumnSize = 25\n\n // If the prefixColumnSize is not provided, we calculate it based on the longest process prefix\n const columnSize =\n prefixColumnSize ??\n processes.reduce((maxPrefixLength, process) => Math.max(maxPrefixLength, process.prefix.length), 0)\n\n // Apply overall limit to the prefix column size\n return Math.min(columnSize, maxColumnSize)\n }, [processes, prefixColumnSize])\n\n const addPrefix = (prefix: string, prefixes: string[]) => {\n const index = prefixes.indexOf(prefix)\n if (index !== -1) {\n return index\n }\n prefixes.push(prefix)\n return prefixes.length - 1\n }\n\n const lineColor = useCallback(\n (index: number) => {\n const colorIndex = index % concurrentColors.length\n return concurrentColors[colorIndex]\n },\n [concurrentColors],\n )\n\n const writableStream = useCallback(\n (process: OutputProcess, prefixes: string[]) => {\n return new Writable({\n write(chunk, _encoding, next) {\n const context = outputContextStore.getStore()\n const prefix = context?.outputPrefix ?? process.prefix\n const shouldStripAnsi = context?.stripAnsi ?? true\n const log = chunk.toString('utf8').replace(/(\\n)$/, '')\n\n const index = addPrefix(prefix, prefixes)\n\n const lines = shouldStripAnsi ? stripAnsi(log).split(/\\n/) : log.split(/\\n/)\n setProcessOutput((previousProcessOutput) => [\n ...previousProcessOutput,\n {\n color: lineColor(index),\n prefix,\n lines,\n },\n ])\n next()\n },\n })\n },\n [lineColor],\n )\n\n const formatPrefix = (prefix: string) => {\n // Truncate prefix if needed\n if (prefix.length > calculatedPrefixColumnSize) {\n return prefix.substring(0, calculatedPrefixColumnSize)\n }\n\n return `${' '.repeat(calculatedPrefixColumnSize - prefix.length)}${prefix}`\n }\n\n useEffect(() => {\n const runProcesses = async () => {\n const prefixes: string[] = []\n\n try {\n await Promise.all(\n processes.map(async (process) => {\n const stdout = writableStream(process, prefixes)\n const stderr = writableStream(process, prefixes)\n await process.action(stdout, stderr, abortSignal)\n }),\n )\n if (!keepRunningAfterProcessesResolve) {\n setCompletionResult({})\n }\n // eslint-disable-next-line no-catch-all/no-catch-all\n } catch (error: unknown) {\n if (!keepRunningAfterProcessesResolve) {\n setCompletionResult({error: error as Error})\n }\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n runProcesses()\n }, [abortSignal, processes, writableStream, keepRunningAfterProcessesResolve])\n\n useEffect(() => {\n if (completionResult !== null) {\n complete(completionResult.error)\n }\n }, [completionResult, complete])\n\n const {lineVertical} = figures\n\n return (\n <Static items={processOutput}>\n {(chunk, index) => {\n return (\n <Box flexDirection=\"column\" key={index}>\n {chunk.lines.map((line, index) => (\n <Box key={index} flexDirection=\"row\">\n <Text>\n {showTimestamps ? (\n <Text>\n {currentTime()} {lineVertical}{' '}\n </Text>\n ) : null}\n <Text color={chunk.color}>{formatPrefix(chunk.prefix)}</Text>\n <Text>\n {' '}\n {lineVertical} {line}\n </Text>\n </Text>\n </Box>\n ))}\n </Box>\n )\n }}\n </Static>\n )\n}\nexport {ConcurrentOutput, ConcurrentOutputContext, useConcurrentOutputContext}\n"]}
1
+ {"version":3,"file":"ConcurrentOutput.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/ConcurrentOutput.tsx"],"names":[],"mappings":"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","sourcesContent":["import {ConcurrentOutputContext, getConcurrentOutputContext, useConcurrentOutputContext} from '../../output-context.js'\nimport {outputInfo, type OutputProcess} from '../../../../public/node/output.js'\nimport {AbortSignal} from '../../../../public/node/abort.js'\nimport {useComplete} from '../../ui.js'\nimport React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react'\nimport {Box, Static, Text, TextProps} from 'ink'\nimport figures from 'figures'\nimport stripAnsi from 'strip-ansi'\n\nimport {Writable} from 'stream'\nimport {StringDecoder} from 'node:string_decoder'\n\nexport interface ConcurrentOutputProps {\n processes: OutputProcess[]\n prefixColumnSize?: number\n abortSignal: AbortSignal\n showTimestamps?: boolean\n /**\n * Keeps terminal UI running after all processes finish. Defaults to false.\n * In JSON mode, false uses finite progress/diagnostic events; true retains streaming terminal UI.\n */\n keepRunningAfterProcessesResolve?: boolean\n useAlternativeColorPalette?: boolean\n}\n\ninterface Chunk {\n color: TextProps['color']\n prefix: string\n lines: string[]\n}\n\nfunction addLeadingZero(number: number) {\n if (number < 10) {\n return `0${number}`\n } else {\n return number.toString()\n }\n}\n\nfunction currentTime() {\n const currentDateTime = new Date()\n const hours = addLeadingZero(currentDateTime.getHours())\n const minutes = addLeadingZero(currentDateTime.getMinutes())\n const seconds = addLeadingZero(currentDateTime.getSeconds())\n return `${hours}:${minutes}:${seconds}`\n}\n\n/** Runs finite processes concurrently and routes their output through the shared diagnostic context. */\nexport async function runConcurrentProcessesForJson({\n processes,\n abortSignal,\n}: Pick<ConcurrentOutputProps, 'processes' | 'abortSignal'>): Promise<void> {\n await Promise.all(\n processes.map(async (process) => {\n const createStream = () => {\n const decoder = new StringDecoder('utf8')\n let pending = ''\n let hasPendingLine = false\n let prefix = process.prefix\n const emitLine = (line: string) => {\n const message = stripAnsi(line)\n if (message.trim().length > 0) outputInfo(`${prefix}: ${message}`)\n }\n const stream = new Writable({\n write(chunk, _encoding, next) {\n if (chunk.length === 0) {\n next()\n return\n }\n const currentPrefix = getConcurrentOutputContext()?.outputPrefix ?? process.prefix\n if (!hasPendingLine) prefix = currentPrefix\n const lines = (pending + decoder.write(chunk)).split(/\\r?\\n/)\n pending = lines.pop() ?? ''\n for (const line of lines) {\n emitLine(line)\n prefix = currentPrefix\n }\n hasPendingLine = chunk[chunk.length - 1] !== 10\n next()\n },\n })\n return {\n stream,\n flush: () => {\n emitLine((pending + decoder.end()).replace(/\\r$/, ''))\n pending = ''\n hasPendingLine = false\n },\n }\n }\n const stdout = createStream()\n const stderr = createStream()\n try {\n await process.action(stdout.stream, stderr.stream, abortSignal)\n } finally {\n stdout.flush()\n stderr.flush()\n }\n }),\n )\n}\n\n/**\n * Renders output from concurrent processes to the terminal.\n * Output will be divided in a three column layout\n * with the left column containing the timestamp,\n * the right column containing the output,\n * and the middle column containing the process prefix.\n * Every process will be rendered with a different color, up to 4 colors.\n *\n * For example running `shopify app dev`:\n *\n * ```shell\n * 2022-10-10 13:11:03 | backend | npm\n * 2022-10-10 13:11:03 | backend | WARN ignoring workspace config at ...\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | backend | > shopify-app-template-node@0.1.0 dev\n * 2022-10-10 13:11:03 | backend | > cross-env NODE_ENV=development nodemon backend/index.js --watch ./backend\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | frontend |\n * 2022-10-10 13:11:03 | frontend | > starter-react-frontend-app@0.1.0 dev\n * 2022-10-10 13:11:03 | frontend | > cross-env NODE_ENV=development node vite-server.js\n * 2022-10-10 13:11:03 | frontend |\n * 2022-10-10 13:11:03 | frontend |\n * 2022-10-10 13:11:03 | backend |\n * 2022-10-10 13:11:03 | backend | [nodemon] to restart at any time, enter `rs`\n * 2022-10-10 13:11:03 | backend | [nodemon] watching path(s): backend/\n * 2022-10-10 13:11:03 | backend | [nodemon] watching extensions: js,mjs,json\n * 2022-10-10 13:11:03 | backend | [nodemon] starting `node backend/index.js`\n * 2022-10-10 13:11:03 | backend |\n *\n * ```\n */\nconst ConcurrentOutput: FunctionComponent<ConcurrentOutputProps> = ({\n processes,\n prefixColumnSize,\n abortSignal,\n showTimestamps = true,\n keepRunningAfterProcessesResolve = false,\n useAlternativeColorPalette = false,\n}) => {\n const [processOutput, setProcessOutput] = useState<Chunk[]>([])\n const [completionResult, setCompletionResult] = useState<{error?: Error} | null>(null)\n const complete = useComplete()\n const concurrentColors: TextProps['color'][] = useMemo(\n () =>\n useAlternativeColorPalette\n ? ['#b994c3', '#e69e19', '#d17a73', 'cyan', 'magenta', 'blue']\n : ['yellow', 'cyan', 'magenta', 'green', 'blue'],\n [useAlternativeColorPalette],\n )\n\n const calculatedPrefixColumnSize = useMemo(() => {\n const maxColumnSize = 25\n\n // If the prefixColumnSize is not provided, we calculate it based on the longest process prefix\n const columnSize =\n prefixColumnSize ??\n processes.reduce((maxPrefixLength, process) => Math.max(maxPrefixLength, process.prefix.length), 0)\n\n // Apply overall limit to the prefix column size\n return Math.min(columnSize, maxColumnSize)\n }, [processes, prefixColumnSize])\n\n const addPrefix = (prefix: string, prefixes: string[]) => {\n const index = prefixes.indexOf(prefix)\n if (index !== -1) {\n return index\n }\n prefixes.push(prefix)\n return prefixes.length - 1\n }\n\n const lineColor = useCallback(\n (index: number) => {\n const colorIndex = index % concurrentColors.length\n return concurrentColors[colorIndex]\n },\n [concurrentColors],\n )\n\n const writableStream = useCallback(\n (process: OutputProcess, prefixes: string[]) => {\n return new Writable({\n write(chunk, _encoding, next) {\n const context = getConcurrentOutputContext()\n const prefix = context?.outputPrefix ?? process.prefix\n const shouldStripAnsi = context?.stripAnsi ?? true\n const log = chunk.toString('utf8').replace(/(\\n)$/, '')\n\n const index = addPrefix(prefix, prefixes)\n\n const lines = shouldStripAnsi ? stripAnsi(log).split(/\\n/) : log.split(/\\n/)\n setProcessOutput((previousProcessOutput) => [\n ...previousProcessOutput,\n {\n color: lineColor(index),\n prefix,\n lines,\n },\n ])\n next()\n },\n })\n },\n [lineColor],\n )\n\n const formatPrefix = (prefix: string) => {\n // Truncate prefix if needed\n if (prefix.length > calculatedPrefixColumnSize) {\n return prefix.substring(0, calculatedPrefixColumnSize)\n }\n\n return `${' '.repeat(calculatedPrefixColumnSize - prefix.length)}${prefix}`\n }\n\n useEffect(() => {\n const runProcesses = async () => {\n const prefixes: string[] = []\n\n try {\n await Promise.all(\n processes.map(async (process) => {\n const stdout = writableStream(process, prefixes)\n const stderr = writableStream(process, prefixes)\n await process.action(stdout, stderr, abortSignal)\n }),\n )\n if (!keepRunningAfterProcessesResolve) {\n setCompletionResult({})\n }\n // eslint-disable-next-line no-catch-all/no-catch-all\n } catch (error: unknown) {\n if (!keepRunningAfterProcessesResolve) {\n setCompletionResult({error: error as Error})\n }\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n runProcesses()\n }, [abortSignal, processes, writableStream, keepRunningAfterProcessesResolve])\n\n useEffect(() => {\n if (completionResult !== null) {\n complete(completionResult.error)\n }\n }, [completionResult, complete])\n\n const {lineVertical} = figures\n\n return (\n <Static items={processOutput}>\n {(chunk, index) => {\n return (\n <Box flexDirection=\"column\" key={index}>\n {chunk.lines.map((line, index) => (\n <Box key={index} flexDirection=\"row\">\n <Text>\n {showTimestamps ? (\n <Text>\n {currentTime()} {lineVertical}{' '}\n </Text>\n ) : null}\n <Text color={chunk.color}>{formatPrefix(chunk.prefix)}</Text>\n <Text>\n {' '}\n {lineVertical} {line}\n </Text>\n </Text>\n </Box>\n ))}\n </Box>\n )\n }}\n </Static>\n )\n}\nexport {ConcurrentOutput, ConcurrentOutputContext, getConcurrentOutputContext, useConcurrentOutputContext}\n"]}
@@ -1,28 +1,22 @@
1
- import { TextAnimation } from './TextAnimation.js';
2
- import useLayout from '../hooks/use-layout.js';
1
+ import { LoadingIndicator } from './LoadingIndicator.js';
3
2
  import { shouldDisplayColors } from '../../../../public/node/output.js';
4
3
  import React from 'react';
5
- import { Box, Text, useStdout } from 'ink';
6
- const loadingBarChar = '▀';
7
- const hillString = '▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁';
4
+ import { Text, useStdout } from 'ink';
8
5
  const LoadingBar = ({ title, noColor, noProgressBar }) => {
9
- const { twoThirds } = useLayout();
10
6
  const { stdout } = useStdout();
11
7
  // On real Node streams, isTTY is only present as an own property when the
12
8
  // stream IS a TTY. When Ink's output stream is not a TTY (e.g. AI agents
13
- // capturing stderr via 2>&1), the animated progress bar can't overwrite
9
+ // capturing stderr via 2>&1), the animated loading indicator can't overwrite
14
10
  // previous frames and would flood the output. Show only the static title
15
11
  // in that case.
16
12
  const isTTY = Boolean(stdout.isTTY);
17
- let loadingBar = new Array(twoThirds).fill(loadingBarChar).join('');
18
- if (noColor ?? !shouldDisplayColors()) {
19
- loadingBar = hillString.repeat(Math.ceil(twoThirds / hillString.length));
20
- }
21
- return (React.createElement(Box, { flexDirection: "column" },
22
- isTTY && !noProgressBar && React.createElement(TextAnimation, { text: loadingBar, maxWidth: twoThirds }),
23
- React.createElement(Text, null,
13
+ const colorsDisabled = noColor ?? !shouldDisplayColors();
14
+ if (!isTTY || noProgressBar) {
15
+ return React.createElement(Text, null,
24
16
  title,
25
- " ...")));
17
+ " ...");
18
+ }
19
+ return React.createElement(LoadingIndicator, { title: title, noColor: colorsDisabled });
26
20
  };
27
21
  export { LoadingBar };
28
22
  //# sourceMappingURL=LoadingBar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"LoadingBar.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/LoadingBar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAA;AAChD,OAAO,SAAS,MAAM,wBAAwB,CAAA;AAC9C,OAAO,EAAC,mBAAmB,EAAC,MAAM,mCAAmC,CAAA;AACrE,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAC,GAAG,EAAE,IAAI,EAAE,SAAS,EAAC,MAAM,KAAK,CAAA;AAExC,MAAM,cAAc,GAAG,GAAG,CAAA;AAC1B,MAAM,UAAU,GAAG,gCAAgC,CAAA;AAQnD,MAAM,UAAU,GAAG,CAAC,EAAC,KAAK,EAAE,OAAO,EAAE,aAAa,EAA2C,EAAE,EAAE;IAC/F,MAAM,EAAC,SAAS,EAAC,GAAG,SAAS,EAAE,CAAA;IAC/B,MAAM,EAAC,MAAM,EAAC,GAAG,SAAS,EAAE,CAAA;IAE5B,0EAA0E;IAC1E,0EAA0E;IAC1E,wEAAwE;IACxE,0EAA0E;IAC1E,gBAAgB;IAChB,MAAM,KAAK,GAAG,OAAO,CAAE,MAA6C,CAAC,KAAK,CAAC,CAAA;IAE3E,IAAI,UAAU,GAAG,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACnE,IAAI,OAAO,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;QACtC,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;IAC1E,CAAC;IAED,OAAO,CACL,oBAAC,GAAG,IAAC,aAAa,EAAC,QAAQ;QACxB,KAAK,IAAI,CAAC,aAAa,IAAI,oBAAC,aAAa,IAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,GAAI;QACpF,oBAAC,IAAI;YAAE,KAAK;mBAAY,CACpB,CACP,CAAA;AACH,CAAC,CAAA;AAED,OAAO,EAAC,UAAU,EAAC,CAAA","sourcesContent":["import {TextAnimation} from './TextAnimation.js'\nimport useLayout from '../hooks/use-layout.js'\nimport {shouldDisplayColors} from '../../../../public/node/output.js'\nimport React from 'react'\n\nimport {Box, Text, useStdout} from 'ink'\n\nconst loadingBarChar = '▀'\nconst hillString = '▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁'\n\ninterface LoadingBarProps {\n title: string\n noColor?: boolean\n noProgressBar?: boolean\n}\n\nconst LoadingBar = ({title, noColor, noProgressBar}: React.PropsWithChildren<LoadingBarProps>) => {\n const {twoThirds} = useLayout()\n const {stdout} = useStdout()\n\n // On real Node streams, isTTY is only present as an own property when the\n // stream IS a TTY. When Ink's output stream is not a TTY (e.g. AI agents\n // capturing stderr via 2>&1), the animated progress bar can't overwrite\n // previous frames and would flood the output. Show only the static title\n // in that case.\n const isTTY = Boolean((stdout as unknown as Record<string, unknown>).isTTY)\n\n let loadingBar = new Array(twoThirds).fill(loadingBarChar).join('')\n if (noColor ?? !shouldDisplayColors()) {\n loadingBar = hillString.repeat(Math.ceil(twoThirds / hillString.length))\n }\n\n return (\n <Box flexDirection=\"column\">\n {isTTY && !noProgressBar && <TextAnimation text={loadingBar} maxWidth={twoThirds} />}\n <Text>{title} ...</Text>\n </Box>\n )\n}\n\nexport {LoadingBar}\n"]}
1
+ {"version":3,"file":"LoadingBar.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/LoadingBar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAC,mBAAmB,EAAC,MAAM,mCAAmC,CAAA;AACrE,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAC,IAAI,EAAE,SAAS,EAAC,MAAM,KAAK,CAAA;AAQnC,MAAM,UAAU,GAAG,CAAC,EAAC,KAAK,EAAE,OAAO,EAAE,aAAa,EAA2C,EAAE,EAAE;IAC/F,MAAM,EAAC,MAAM,EAAC,GAAG,SAAS,EAAE,CAAA;IAE5B,0EAA0E;IAC1E,0EAA0E;IAC1E,6EAA6E;IAC7E,0EAA0E;IAC1E,gBAAgB;IAChB,MAAM,KAAK,GAAG,OAAO,CAAE,MAA6C,CAAC,KAAK,CAAC,CAAA;IAC3E,MAAM,cAAc,GAAG,OAAO,IAAI,CAAC,mBAAmB,EAAE,CAAA;IAExD,IAAI,CAAC,KAAK,IAAI,aAAa,EAAE,CAAC;QAC5B,OAAO,oBAAC,IAAI;YAAE,KAAK;mBAAY,CAAA;IACjC,CAAC;IAED,OAAO,oBAAC,gBAAgB,IAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,cAAc,GAAI,CAAA;AACpE,CAAC,CAAA;AAED,OAAO,EAAC,UAAU,EAAC,CAAA","sourcesContent":["import {LoadingIndicator} from './LoadingIndicator.js'\nimport {shouldDisplayColors} from '../../../../public/node/output.js'\nimport React from 'react'\n\nimport {Text, useStdout} from 'ink'\n\ninterface LoadingBarProps {\n title: string\n noColor?: boolean\n noProgressBar?: boolean\n}\n\nconst LoadingBar = ({title, noColor, noProgressBar}: React.PropsWithChildren<LoadingBarProps>) => {\n const {stdout} = useStdout()\n\n // On real Node streams, isTTY is only present as an own property when the\n // stream IS a TTY. When Ink's output stream is not a TTY (e.g. AI agents\n // capturing stderr via 2>&1), the animated loading indicator can't overwrite\n // previous frames and would flood the output. Show only the static title\n // in that case.\n const isTTY = Boolean((stdout as unknown as Record<string, unknown>).isTTY)\n const colorsDisabled = noColor ?? !shouldDisplayColors()\n\n if (!isTTY || noProgressBar) {\n return <Text>{title} ...</Text>\n }\n\n return <LoadingIndicator title={title} noColor={colorsDisabled} />\n}\n\nexport {LoadingBar}\n"]}
@@ -2,10 +2,9 @@ import { LoadingBar } from './LoadingBar.js';
2
2
  import { Stdout } from '../../ui.js';
3
3
  import { render } from '../../testing/ui.js';
4
4
  import { shouldDisplayColors, unstyled } from '../../../../public/node/output.js';
5
- import useLayout from '../hooks/use-layout.js';
6
- import React from 'react';
5
+ import React, { act } from 'react';
6
+ import chalk from 'chalk';
7
7
  import { beforeEach, describe, expect, test, vi } from 'vitest';
8
- vi.mock('../hooks/use-layout.js');
9
8
  vi.mock('../../../../public/node/output.js', async () => {
10
9
  const original = await vi.importActual('../../../../public/node/output.js');
11
10
  return {
@@ -14,130 +13,69 @@ vi.mock('../../../../public/node/output.js', async () => {
14
13
  };
15
14
  });
16
15
  beforeEach(() => {
17
- vi.mocked(useLayout).mockReturnValue({
18
- twoThirds: 53,
19
- oneThird: 27,
20
- fullWidth: 80,
21
- });
22
16
  vi.mocked(shouldDisplayColors).mockReturnValue(true);
23
17
  });
24
- /**
25
- * Creates a Stdout test double simulating a TTY stream.
26
- * On real Node streams, isTTY is only present as an own property when the
27
- * stream IS a TTY.
28
- */
29
- function createTTYStdout(columns = 100) {
30
- const stdout = new Stdout({ columns });
18
+ function createTTYStdout() {
19
+ const stdout = new Stdout({ columns: 100 });
31
20
  stdout.isTTY = true;
32
21
  return stdout;
33
22
  }
34
- /**
35
- * Renders LoadingBar with a TTY stdout so the animated progress bar renders.
36
- */
37
23
  function renderWithTTY(element) {
38
24
  const stdout = createTTYStdout();
39
25
  const instance = render(element, { stdout });
40
26
  return { lastFrame: stdout.lastFrame, unmount: instance.unmount };
41
27
  }
42
28
  describe('LoadingBar', () => {
43
- test('renders loading bar with default colored characters', async () => {
44
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Loading content" }));
45
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
46
- "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
47
- Loading content ..."
48
- `);
49
- });
50
- test('renders loading bar with hill pattern when noColor prop is true', async () => {
51
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Processing files", noColor: true }));
52
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
53
- "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅
54
- Processing files ..."
55
- `);
56
- });
57
- test('renders loading bar with hill pattern when shouldDisplayColors returns false', async () => {
58
- vi.mocked(shouldDisplayColors).mockReturnValue(false);
59
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Downloading packages" }));
60
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
61
- "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅
62
- Downloading packages ..."
63
- `);
64
- });
65
- test('handles narrow terminal width correctly', async () => {
66
- vi.mocked(useLayout).mockReturnValue({ twoThirds: 20, oneThird: 10, fullWidth: 30 });
67
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Building app" }));
68
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
69
- "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
70
- Building app ..."
71
- `);
29
+ test('renders the Shopify loading indicator', async () => {
30
+ const { lastFrame, unmount } = renderWithTTY(React.createElement(LoadingBar, { title: "Loading content" }));
31
+ const frame = lastFrame();
32
+ expect(unstyled(frame)).toBe('S> Loading content ...');
33
+ expect(frame).toContain('\u001B[1m');
34
+ expect(frame).toContain('\u001B[3m');
35
+ expect(frame).toContain(chalk.hex('#96BF48')('>'));
36
+ unmount();
72
37
  });
73
- test('handles narrow terminal width correctly in no-color mode', async () => {
74
- vi.mocked(useLayout).mockReturnValue({ twoThirds: 15, oneThird: 8, fullWidth: 23 });
75
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Installing", noColor: true }));
76
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
77
- "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇
78
- Installing ..."
79
- `);
38
+ test('blinks the chevron without shifting the title', async () => {
39
+ vi.useFakeTimers();
40
+ const { lastFrame, unmount } = renderWithTTY(React.createElement(LoadingBar, { title: "Uploading theme" }));
41
+ try {
42
+ await act(async () => {
43
+ await vi.advanceTimersByTimeAsync(350);
44
+ });
45
+ expect(unstyled(lastFrame())).toBe('S Uploading theme ...');
46
+ }
47
+ finally {
48
+ unmount();
49
+ vi.useRealTimers();
50
+ }
80
51
  });
81
- test('handles very narrow terminal width in no-color mode', async () => {
82
- vi.mocked(useLayout).mockReturnValue({ twoThirds: 5, oneThird: 3, fullWidth: 8 });
83
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Wait", noColor: true }));
84
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
85
- "▁▁▁▂▂
86
- Wait ..."
87
- `);
52
+ test('renders the chevron without color when noColor is true', async () => {
53
+ const { lastFrame, unmount } = renderWithTTY(React.createElement(LoadingBar, { title: "Processing files", noColor: true }));
54
+ const frame = lastFrame();
55
+ expect(unstyled(frame)).toBe('S> Processing files ...');
56
+ expect(frame).not.toContain(chalk.hex('#96BF48')('>'));
57
+ unmount();
88
58
  });
89
- test('handles wide terminal width correctly', async () => {
90
- vi.mocked(useLayout).mockReturnValue({ twoThirds: 100, oneThird: 50, fullWidth: 150 });
91
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Synchronizing data" }));
92
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
93
- "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
94
- Synchronizing data ..."
95
- `);
96
- });
97
- test('handles wide terminal width correctly in no-color mode with pattern repetition', async () => {
98
- vi.mocked(useLayout).mockReturnValue({ twoThirds: 90, oneThird: 45, fullWidth: 135 });
99
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Analyzing dependencies", noColor: true }));
100
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
101
- "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁
102
- Analyzing dependencies ..."
103
- `);
104
- });
105
- test('renders correctly with empty title', async () => {
106
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "" }));
107
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
108
- "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
109
- ..."
110
- `);
111
- });
112
- test('noColor prop overrides shouldDisplayColors when both would show colors', async () => {
113
- vi.mocked(shouldDisplayColors).mockReturnValue(true);
114
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Testing override", noColor: true }));
115
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`
116
- "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅
117
- Testing override ..."
118
- `);
59
+ test('renders the chevron without color when colors are disabled', async () => {
60
+ vi.mocked(shouldDisplayColors).mockReturnValue(false);
61
+ const { lastFrame, unmount } = renderWithTTY(React.createElement(LoadingBar, { title: "Downloading packages" }));
62
+ const frame = lastFrame();
63
+ expect(unstyled(frame)).toBe('S> Downloading packages ...');
64
+ expect(frame).not.toContain(chalk.hex('#96BF48')('>'));
65
+ unmount();
119
66
  });
120
- test('renders consistently with same props', async () => {
121
- const props = { title: 'Consistent test', noColor: false };
122
- const { lastFrame: frame1 } = renderWithTTY(React.createElement(LoadingBar, { ...props }));
123
- const { lastFrame: frame2 } = renderWithTTY(React.createElement(LoadingBar, { ...props }));
124
- expect(frame1()).toBe(frame2());
67
+ test('renders correctly with an empty title', async () => {
68
+ const { lastFrame, unmount } = renderWithTTY(React.createElement(LoadingBar, { title: "" }));
69
+ expect(unstyled(lastFrame())).toBe('S> ...');
70
+ unmount();
125
71
  });
126
- test('hides progress bar when noProgressBar is true', async () => {
127
- vi.mocked(shouldDisplayColors).mockReturnValue(true);
72
+ test('hides the loading indicator when noProgressBar is true', async () => {
128
73
  const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "task 1", noProgressBar: true }));
129
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`"task 1 ..."`);
74
+ expect(unstyled(lastFrame())).toBe('task 1 ...');
130
75
  });
131
76
  test('shows only static title text when output stream is not a TTY', async () => {
132
- // Default test Stdout has no isTTY property, simulating a non-TTY stream
133
77
  const { lastFrame } = render(React.createElement(LoadingBar, { title: "Installing dependencies" }));
134
- expect(unstyled(lastFrame())).toMatchInlineSnapshot(`"Installing dependencies ..."`);
135
- });
136
- test('shows animated progress bar when output stream is a TTY', async () => {
137
- const { lastFrame } = renderWithTTY(React.createElement(LoadingBar, { title: "Uploading theme" }));
138
- const frame = unstyled(lastFrame());
139
- expect(frame).toContain('▀');
140
- expect(frame).toContain('Uploading theme ...');
78
+ expect(unstyled(lastFrame())).toBe('Installing dependencies ...');
141
79
  });
142
80
  });
143
81
  //# sourceMappingURL=LoadingBar.test.js.map