Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @astrojs/upgrade package for automatic package upgrades #8525

Merged
merged 33 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fc7c911
spike(upgrade): add WIP upgrade package
natemoo-re Sep 1, 2023
cf1591f
feat: update upgrade cli
natemoo-re Sep 12, 2023
d36b4a8
chore: delete tests
natemoo-re Sep 12, 2023
9ad8059
chore: remove unused dependency
natemoo-re Sep 12, 2023
6095f53
chore: appease typescript
natemoo-re Sep 15, 2023
73f881c
wip: add confirmation before updating major
natemoo-re Sep 18, 2023
79b3ec0
feat(upgrade): add changelog links
natemoo-re Nov 27, 2023
9394dcf
fix(upgrade): ensure install is aborted on exit
natemoo-re Nov 27, 2023
87a62f0
fix(upgrade): add terminal-link dependency
natemoo-re Nov 27, 2023
42b98f8
docs(upgrade): update README
natemoo-re Nov 27, 2023
0233b17
chore: update lockfile
natemoo-re Nov 27, 2023
feb3199
fix(upgrade): return exit
natemoo-re Nov 27, 2023
b045794
chore(upgrade): add basic test suite
natemoo-re Nov 27, 2023
d6949f5
test(upgrade): fix failing tests
natemoo-re Nov 27, 2023
1a9e27c
chore: updrade lockfile
natemoo-re Nov 27, 2023
28e9e79
chore(upgrade): make terminal-link a regular dep
natemoo-re Nov 27, 2023
72153df
fix(upgrade): better handling for prereleases
natemoo-re Nov 27, 2023
8c97f43
chore: add changeset
natemoo-re Nov 27, 2023
586c656
Update tasty-dryers-bathe.md
natemoo-re Nov 27, 2023
3ec461e
chore: change case
natemoo-re Nov 27, 2023
1a420e1
Update packages/upgrade/README.md
natemoo-re Nov 27, 2023
619ad95
Update packages/upgrade/README.md
natemoo-re Nov 27, 2023
3619c1a
fix(upgrade): fix version comparison
natemoo-re Nov 27, 2023
ce949d4
chore: fix tsconfig
natemoo-re Nov 27, 2023
c154e7d
Update packages/upgrade/README.md
natemoo-re Nov 27, 2023
1535029
chore: update changeset
natemoo-re Nov 27, 2023
377a543
refactor: replace `log('')` with `newline()`
natemoo-re Nov 27, 2023
2a8bf4f
refactor: use existing array
natemoo-re Nov 27, 2023
27d9409
refactor: add comment for sortPackages
natemoo-re Nov 27, 2023
bd9c2d8
refactor: add comment for ensureYarnLock
natemoo-re Nov 27, 2023
06f8bd1
refactor: update comment with link to original PR
natemoo-re Nov 27, 2023
f1a16c9
refactor: add comment for ensureYarnLock
natemoo-re Nov 27, 2023
203530f
refactor: make verify more defensive
natemoo-re Nov 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/tasty-dryers-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@astrojs/upgrade': minor
---

Initial release!

`@astrojs/upgrade` is an automated command-line tool for upgrading Astro and your official Astro integrations together.

Inside of your existing `astro` project, run the following command to install the `latest` version of your integrations.

**With NPM:**

```bash
npx @astrojs/upgrade
```

**With Yarn:**

```bash
yarn dlx @astrojs/upgrade
```

**With PNPM:**

```bash
pnpm dlx @astrojs/upgrade
```
53 changes: 53 additions & 0 deletions packages/upgrade/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# @astrojs/upgrade

A command-line tool for upgrading your Astro integrations and dependencies.
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved

You can run this command in your terminal to upgrade your official Astro integrations at the same time you upgrade your version of Astro.

## Usage

`@astrojs/upgrade` should not be added as a dependency to your project, but run as a temporary executable whenever you want to upgrade using [`npx`](https://docs.npmjs.com/cli/v10/commands/npx) or [`dlx`](https://pnpm.io/cli/dlx).

**With NPM:**

```bash
npx @astrojs/upgrade
```

**With Yarn:**

```bash
yarn dlx @astrojs/upgrade
```

**With PNPM:**

```bash
pnpm dlx @astrojs/upgrade
```

## Options

### tag (optional)

It is possible to pass a specific `tag` to resolve packages against. If not included, `@astrojs/upgrade` looks for the `latest` tag.

For example, Astro often releases `beta` versions prior to an upcoming major release. Upgrade an existing Astro project and it's dependencies to the `beta` version using one of the following commands:

**With NPM:**

```bash
npx @astrojs/upgrade beta
```

**With Yarn:**

```bash
yarn dlx @astrojs/upgrade beta
```

**With PNPM:**

```bash
pnpm dlx @astrojs/upgrade beta
```
49 changes: 49 additions & 0 deletions packages/upgrade/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@astrojs/upgrade",
"version": "0.0.1",
"type": "module",
"author": "withastro",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"directory": "packages/upgrade"
},
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://astro.build",
"exports": {
".": "./upgrade.mjs"
},
"main": "./upgrade.mjs",
"bin": "./upgrade.mjs",
"scripts": {
"build": "astro-scripts build \"src/index.ts\" --bundle && tsc",
"build:ci": "astro-scripts build \"src/index.ts\" --bundle",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "mocha --exit --timeout 20000 --parallel"
},
"files": [
"dist",
"upgrade.js"
],
"//a": "MOST PACKAGES SHOULD GO IN DEV_DEPENDENCIES! THEY WILL BE BUNDLED.",
"//b": "DEPENDENCIES IS FOR UNBUNDLED PACKAGES",
"dependencies": {
"@astrojs/cli-kit": "^0.2.3",
"semver": "^7.5.4",
"which-pm-runs": "^1.1.0",
"terminal-link": "^3.0.0"
},
"devDependencies": {
"@types/semver": "^7.5.2",
"@types/which-pm-runs": "^1.0.0",
"arg": "^5.0.2",
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
"mocha": "^10.2.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18.14.1"
}
}
56 changes: 56 additions & 0 deletions packages/upgrade/src/actions/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { prompt } from '@astrojs/cli-kit';
import arg from 'arg';
import { pathToFileURL } from 'node:url';
import detectPackageManager from 'which-pm-runs';

export interface Context {
help: boolean;
prompt: typeof prompt;
version: string;
dryRun?: boolean;
cwd: URL;
stdin?: typeof process.stdin;
stdout?: typeof process.stdout;
packageManager: string;
packages: PackageInfo[];
exit(code: number): never;
}

export interface PackageInfo {
name: string;
currentVersion: string;
targetVersion: string;
tag?: string;
isDevDependency?: boolean;
isMajor?: boolean;
changelogURL?: string;
changelogTitle?: string;
}

export async function getContext(argv: string[]): Promise<Context> {
const flags = arg(
{
'--dry-run': Boolean,
'--help': Boolean,

'-h': '--help',
},
{ argv, permissive: true }
)

const packageManager = detectPackageManager()?.name ?? 'npm';
const { _: [version = 'latest'] = [], '--help': help = false, '--dry-run': dryRun } = flags;

return {
help,
prompt,
packageManager,
packages: [],
cwd: new URL(pathToFileURL(process.cwd()) + '/'),
dryRun,
version,
exit(code) {
process.exit(code);
},
} satisfies Context
}
15 changes: 15 additions & 0 deletions packages/upgrade/src/actions/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { printHelp } from '../messages.js';

export function help() {
printHelp({
commandName: '@astrojs/upgrade',
usage: '[version] [...flags]',
headline: 'Upgrade Astro dependencies.',
tables: {
Flags: [
['--help (-h)', 'See all available flags.'],
['--dry-run', 'Walk through steps without executing.']
],
},
});
}
125 changes: 125 additions & 0 deletions packages/upgrade/src/actions/install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { Context, PackageInfo } from './context.js';

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { color, say } from '@astrojs/cli-kit';
import { pluralize, celebrations, done, error, info, log, spinner, success, upgrade, banner, title, changelog, warn, bye } from '../messages.js';
import { shell } from '../shell.js';
import { random, sleep } from '@astrojs/cli-kit/utils';

export async function install(
ctx: Pick<Context, 'version' | 'packages' | 'packageManager' | 'prompt' | 'dryRun' | 'exit' | 'cwd'>
) {
await banner();
log('')
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved
const { current, dependencies, devDependencies } = filterPackages(ctx);
const toInstall = [...dependencies, ...devDependencies];
for (const packageInfo of current.sort(sortPackages)) {
const tag = /^\d/.test(packageInfo.targetVersion) ? packageInfo.targetVersion : packageInfo.targetVersion.slice(1)
await info(`${packageInfo.name}`, `is up to date on`, `v${tag}`)
await sleep(random(50, 150));
}
if (toInstall.length === 0 && !ctx.dryRun) {
log('')
await success(random(celebrations), random(done));
return;
}
const majors: PackageInfo[] = []
for (const packageInfo of [...dependencies, ...devDependencies].sort(sortPackages)) {
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved
const word = ctx.dryRun ? 'can' : 'will';
await upgrade(packageInfo, `${word} be updated to`)
if (packageInfo.isMajor) {
majors.push(packageInfo)
}
}
if (majors.length > 0) {
const { proceed } = await ctx.prompt({
name: 'proceed',
type: 'confirm',
label: title('wait'),
message: `${pluralize(['One package has', 'Some packages have'], majors.length)} breaking changes. Continue?`,
initial: true,
});
if (!proceed) {
return ctx.exit(0);
}

log('');

await warn('check', `Be sure to follow the ${pluralize('CHANGELOG', majors.length)}.`);
for (const pkg of majors.sort(sortPackages)) {
await changelog(pkg.name, pkg.changelogTitle!, pkg.changelogURL!);
}
}

log('')
if (ctx.dryRun) {
await info('--dry-run', `Skipping dependency installation`);
} else {
await runInstallCommand(ctx, dependencies, devDependencies);
}
}

function filterPackages(ctx: Pick<Context, 'packages'>) {
const current: PackageInfo[] = [];
const dependencies: PackageInfo[] = [];
const devDependencies: PackageInfo[] = [];
for (const packageInfo of ctx.packages) {
const { currentVersion, targetVersion, isDevDependency } = packageInfo;
// Remove prefix from `currentVersion` before comparing
if (currentVersion.replace(/^\D+/, '') === targetVersion) {
current.push(packageInfo);
} else {
const arr = isDevDependency ? devDependencies : dependencies;
arr.push(packageInfo);
}
}
return { current, dependencies, devDependencies }
}

function sortPackages(a: PackageInfo, b: PackageInfo): number {
if (a.isMajor && !b.isMajor) return 1;
if (b.isMajor && !a.isMajor) return -1;
if (a.name === 'astro') return -1;
if (b.name === 'astro') return 1;
if (a.name.startsWith('@astrojs') && !b.name.startsWith('@astrojs')) return -1;
if (b.name.startsWith('@astrojs') && !a.name.startsWith('@astrojs')) return 1;
return a.name.localeCompare(b.name);
}
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved

async function runInstallCommand(ctx: Pick<Context, 'cwd' | 'packageManager' | 'exit'>, dependencies: PackageInfo[], devDependencies: PackageInfo[]) {
const cwd = fileURLToPath(ctx.cwd);
if (ctx.packageManager === 'yarn') await ensureYarnLock({ cwd });

await spinner({
start: `Installing dependencies with ${ctx.packageManager}...`,
end: `Installed dependencies!`,
while: async () => {
try {
if (dependencies.length > 0) {
await shell(ctx.packageManager, ['install', ...dependencies.map(({ name, targetVersion }) => `${name}@${(targetVersion).replace(/^\^/, '')}`)], { cwd, timeout: 90_000, stdio: 'ignore' })
}
if (devDependencies.length > 0) {
await shell(ctx.packageManager, ['install', '--save-dev', ...devDependencies.map(({ name, targetVersion }) => `${name}@${(targetVersion).replace(/^\^/, '')}`)], { cwd, timeout: 90_000, stdio: 'ignore' })
}
} catch {
const packages = [...dependencies, ...devDependencies].map(({ name, targetVersion }) => `${name}@${targetVersion}`).join(' ')
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved
log('');
error(
'error',
`Dependencies failed to install, please run the following command manually:\n${color.bold(`${ctx.packageManager} install ${packages}`)}`
);
return ctx.exit(1);
}
},
});

await say([`${random(celebrations)} ${random(done)}`, random(bye)], { clear: false });
}

async function ensureYarnLock({ cwd }: { cwd: string }) {
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved
const yarnLock = path.join(cwd, 'yarn.lock');
if (fs.existsSync(yarnLock)) return;
return fs.promises.writeFile(yarnLock, '', { encoding: 'utf-8' });
}
Loading
Loading