-
Notifications
You must be signed in to change notification settings - Fork 249
Expand file tree
/
Copy pathupgrade.ts
More file actions
238 lines (212 loc) · 8.6 KB
/
upgrade.ts
File metadata and controls
238 lines (212 loc) · 8.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import {isDevelopment} from './context/local.js'
import {currentProcessIsGlobal, inferPackageManagerForGlobalCLI, getProjectDir} from './is-global.js'
import {
checkForCachedNewVersion,
findUpAndReadPackageJson,
PackageJson,
checkForNewVersion,
DependencyType,
usesWorkspaces,
addNPMDependencies,
getPackageManagerForProjectRoot,
} from './node-package-manager.js'
import {outputContent, outputDebug, outputInfo, outputToken, outputWarn} from './output.js'
import {cwd, moduleDirectory, sniffForPath} from './path.js'
import {exec, isCI} from './system.js'
import {renderConfirmationPrompt} from './ui.js'
import {isPreReleaseVersion} from './version.js'
import {getAutoUpgradeEnabled, setAutoUpgradeEnabled, runAtMinimumInterval} from '../../private/node/conf-store.js'
import {CLI_KIT_VERSION} from '../common/version.js'
/**
* Utility function for generating an install command for the user to run
* to install an updated version of Shopify CLI.
*
* @returns A string with the command to run.
*/
export function cliInstallCommand(): string | undefined {
const packageManager = inferPackageManagerForGlobalCLI()
if (!packageManager || packageManager === 'unknown') return undefined
if (packageManager === 'homebrew') {
return 'brew upgrade shopify-cli'
} else if (packageManager === 'yarn') {
return `${packageManager} global add @shopify/cli@latest`
} else {
const verb = packageManager === 'pnpm' ? 'add' : 'install'
return `${packageManager} ${verb} -g @shopify/cli@latest`
}
}
/**
* Runs the CLI upgrade using the appropriate package manager.
* Determines the install command and executes it.
*
* @throws AbortError if the package manager or command cannot be determined.
*/
export async function runCLIUpgrade(): Promise<void> {
// Path where the current project is (app/hydrogen)
const path = sniffForPath() ?? cwd()
const projectDir = getProjectDir(path)
// Check if we are running in a global context if not, return
const isGlobal = currentProcessIsGlobal()
// Don't auto-upgrade for development mode
if (!isGlobal && isDevelopment()) {
outputInfo('Skipping upgrade in development mode.')
return
}
// Generate the install command for the global CLI and execute it
if (isGlobal) {
const installCommand = cliInstallCommand()
if (!installCommand) {
throw new Error('Could not determine the package manager')
}
const [command, ...args] = installCommand.split(' ')
if (!command) {
throw new Error('Could not determine the command to run')
}
outputInfo(outputContent`Upgrading Shopify CLI by running: ${outputToken.genericShellCommand(installCommand)}...`)
await exec(command, args, {stdio: 'inherit'})
} else if (projectDir) {
await upgradeLocalShopify(projectDir, CLI_KIT_VERSION)
} else {
throw new Error('Could not determine the local project directory')
}
}
/**
* Returns the version to auto-upgrade to, or undefined if auto-upgrade should be skipped.
* Auto-upgrade is disabled by default and must be enabled via `shopify upgrade`.
* Also skips for CI, pre-release versions, or when no newer version is available.
*
* @returns The version string to upgrade to, or undefined if no upgrade should happen.
*/
export function versionToAutoUpgrade(): string | undefined {
const currentVersion = CLI_KIT_VERSION
const newerVersion = checkForCachedNewVersion('@shopify/cli', currentVersion)
if (!newerVersion) {
outputDebug('Auto-upgrade: No newer version available.')
return undefined
}
if (process.env.SHOPIFY_CLI_FORCE_AUTO_UPGRADE === '1') {
outputDebug('Auto-upgrade: Forcing auto-upgrade because of SHOPIFY_CLI_FORCE_AUTO_UPGRADE.')
return newerVersion
}
if (!getAutoUpgradeEnabled()) {
outputDebug('Auto-upgrade: Skipping because auto-upgrade is not enabled.')
return undefined
}
if (isCI()) {
outputDebug('Auto-upgrade: Skipping auto-upgrade in CI.')
return undefined
}
if (isPreReleaseVersion(currentVersion)) {
outputDebug('Auto-upgrade: Skipping auto-upgrade for pre-release version.')
return undefined
}
return newerVersion
}
/**
* Shows a daily upgrade-available warning for users who have not enabled auto-upgrade.
* Skipped in CI and for pre-release versions. When auto-upgrade is enabled this is a no-op
* because the postrun hook will handle the upgrade directly.
*/
export async function warnIfUpgradeAvailable(): Promise<void> {
if (isCI() || isPreReleaseVersion(CLI_KIT_VERSION) || getAutoUpgradeEnabled()) return
const newerVersion = checkForCachedNewVersion('@shopify/cli', CLI_KIT_VERSION)
if (!newerVersion) return
await runAtMinimumInterval('warn-on-available-upgrade', {days: 1}, async () => {
outputWarn(getOutputUpdateCLIReminder(newerVersion))
})
}
/**
* Generates a message to remind the user to update the CLI.
*
* @param version - The version to update to.
* @returns The message to remind the user to update the CLI.
*/
export function getOutputUpdateCLIReminder(version: string): string {
const installCommand = cliInstallCommand()
if (installCommand) {
return outputContent`💡 Version ${version} available! Run ${outputToken.genericShellCommand(installCommand)}`.value
}
return outputContent`💡 Version ${version} available!`.value
}
/**
* Prompts the user to enable or disable automatic upgrades, then persists their choice.
*
* @returns Whether the user chose to enable auto-upgrade.
*/
export async function promptAutoUpgrade(): Promise<boolean> {
const enabled = await renderConfirmationPrompt({
message: 'Enable automatic updates for Shopify CLI?',
confirmationMessage: 'Yes, automatically update',
cancellationMessage: "No, I'll update manually",
})
setAutoUpgradeEnabled(enabled)
return enabled
}
async function upgradeLocalShopify(projectDir: string, currentVersion: string) {
const packageJson = (await findUpAndReadPackageJson(projectDir)).content
const packageJsonDependencies = packageJson.dependencies ?? {}
const packageJsonDevDependencies = packageJson.devDependencies ?? {}
const allDependencies = {...packageJsonDependencies, ...packageJsonDevDependencies}
let resolvedCLIVersion = allDependencies[await cliDependency()]
if (!resolvedCLIVersion) {
outputDebug('Auto-upgrade: CLI dependency not found in project dependencies, skipping local upgrade.')
return
}
if (resolvedCLIVersion.slice(0, 1).match(/[\^~]/)) resolvedCLIVersion = currentVersion
const newestCLIVersion = await checkForNewVersion(await cliDependency(), resolvedCLIVersion)
if (newestCLIVersion) {
outputUpgradeMessage(resolvedCLIVersion, newestCLIVersion)
} else {
outputWontInstallMessage(resolvedCLIVersion)
}
await installJsonDependencies('prod', packageJsonDependencies, projectDir)
await installJsonDependencies('dev', packageJsonDevDependencies, projectDir)
}
async function installJsonDependencies(
depsEnv: DependencyType,
deps: {[key: string]: string},
directory: string,
): Promise<void> {
const packagesToUpdate = [await cliDependency(), ...(await oclifPlugins())]
.filter((pkg: string): boolean => {
const pkgRequirement: string | undefined = deps[pkg]
return Boolean(pkgRequirement)
})
.map((pkg) => {
return {name: pkg, version: 'latest'}
})
const appUsesWorkspaces = await usesWorkspaces(directory)
if (packagesToUpdate.length > 0) {
await addNPMDependencies(packagesToUpdate, {
packageManager: await getPackageManagerForProjectRoot(directory),
type: depsEnv,
directory,
stdout: process.stdout,
stderr: process.stderr,
addToRootDirectory: appUsesWorkspaces,
})
}
}
async function cliDependency(): Promise<string> {
return (await packageJsonContents()).name
}
async function oclifPlugins(): Promise<string[]> {
return (await packageJsonContents())?.oclif?.plugins ?? []
}
type PackageJsonWithName = Omit<PackageJson, 'name'> & {name: string}
let _packageJsonContents: PackageJsonWithName | undefined
async function packageJsonContents(): Promise<PackageJsonWithName> {
if (!_packageJsonContents) {
const packageJson = await findUpAndReadPackageJson(moduleDirectory(import.meta.url))
_packageJsonContents = _packageJsonContents ?? (packageJson.content as PackageJsonWithName)
}
return _packageJsonContents
}
function outputWontInstallMessage(currentVersion: string): void {
outputInfo(outputContent`You're on the latest version, ${outputToken.yellow(currentVersion)}, no need to upgrade!`)
}
function outputUpgradeMessage(currentVersion: string, newestVersion: string): void {
outputInfo(
outputContent`Upgrading CLI from ${outputToken.yellow(currentVersion)} to ${outputToken.yellow(newestVersion)}...`,
)
}