Skip to content

Commit

Permalink
Enhance CLI command - Update (#406)
Browse files Browse the repository at this point in the history
* initial idea: update cli for users to select the update instead of text.

* update docs on version command

* Update update.ts

* Rework update script to suggest versions

* Update update.ts

* Misc fixes

* Fix typo

* Remove unnecessary logging

* Update Docs - Remove terminal line message

* Update updates.mdx

---------

Co-authored-by: Hayden Bleasel <[email protected]>
  • Loading branch information
carvillanueva and haydenbleasel authored Jan 17, 2025
1 parent 46a348e commit a8f3d83
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 20 deletions.
17 changes: 16 additions & 1 deletion docs/updates.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,22 @@ description: Built-in helpers to help you keep your project up to date.
As next-forge evolves, you may want to stay up to date with the latest changes. This can be difficult to do manually, so we've created a script to help you.

```sh Terminal
npx next-forge@latest update --from 2.18.30 --to 2.18.31
npx next-forge@latest update
```

This will run our update script, which will guide you through the process of updating your project.

```
┌ Let's update your next-forge project!
◆ Select a version to update to:
│ ● v3.2.15
│ ○ v3.2.14
│ ○ v3.2.13
│ ○ v3.2.12
│ ○ v3.2.11
```

This will clone the latest version of next-forge into a temporary directory, apply the updates, and then copy the files over to your project. From here, you can commit the changes and push them to your repository.
Expand Down
2 changes: 1 addition & 1 deletion scripts/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const getName = async () => {
const value = await text({
message: 'What is your project named?',
placeholder: 'my-app',
validate(value) {
validate(value: string) {
if (value.length === 0) {
return 'Please enter a project name.';
}
Expand Down
78 changes: 60 additions & 18 deletions scripts/update.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import { copyFile, mkdir, rm } from 'node:fs/promises';
import { copyFile, mkdir, readFile, rm } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import {
cancel,
intro,
isCancel,
log,
outro,
select,
spinner,
text,
} from '@clack/prompts';
import {
url,
allInternalContent,
cleanFileName,
exec,
semver,
getAvailableVersions,
tempDirName,
} from './utils.js';

const compareVersions = (a: string, b: string) => {
const [aMajor, aMinor, aPatch] = a.split('.').map(Number);
const [bMajor, bMinor, bPatch] = b.split('.').map(Number);
if (aMajor !== bMajor) {
return aMajor - bMajor;
}
if (aMinor !== bMinor) {
return aMinor - bMinor;
}
return aPatch - bPatch;
};

const createTemporaryDirectory = async (name: string) => {
const cwd = process.cwd();
const tempDir = join(cwd, name);
Expand Down Expand Up @@ -56,19 +68,24 @@ const updateFiles = async (files: string[]) => {
const deleteTemporaryDirectory = async () =>
await rm(tempDirName, { recursive: true, force: true });

const getVersion = async (type: 'to' | 'from') => {
const version = await text({
message: `What version are you updating ${type}?`,
placeholder: '1.2.3',
validate: (value) => {
if (value.length === 0) {
return 'Please enter a version.';
}
const getCurrentVersion = async (): Promise<string | undefined> => {
const packageJsonPath = join(process.cwd(), 'package.json');
const packageJsonContents = await readFile(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(packageJsonContents) as { version?: string };

if (!semver.test(value)) {
return 'Please enter a valid version without the "v" e.g. 1.2.3';
}
},
return packageJson.version;
};

const selectVersion = async (
label: string,
availableVersions: string[],
initialValue: string | undefined
) => {
const version = await select({
message: `Select a version to update ${label}:`,
options: availableVersions.map((v) => ({ value: v, label: `v${v}` })),
initialValue,
maxItems: 10,
});

if (isCancel(version)) {
Expand Down Expand Up @@ -110,13 +127,38 @@ const getDiff = async (
return filesToUpdate;
};

export const update = async (options: { from: string; to: string }) => {
export const update = async (options: { from?: string; to?: string }) => {
try {
intro("Let's update your next-forge project!");

const cwd = process.cwd();
const fromVersion = options.from || (await getVersion('from'));
const toVersion = options.to || (await getVersion('to'));
const availableVersions = await getAvailableVersions();
let currentVersion = await getCurrentVersion();

// Ditch the project version if it is not in the available versions
if (currentVersion && !availableVersions.includes(currentVersion)) {
currentVersion = undefined;
}

const fromVersion =
options.from ||
(await selectVersion('from', availableVersions, currentVersion));

if (fromVersion === availableVersions[0]) {
outro('You are already on the latest version!');
return;
}

const upgradeableVersions = availableVersions.filter(
(v) => compareVersions(v, fromVersion) > 0
);

const [nextVersion] = upgradeableVersions;

const toVersion =
options.to ||
(await selectVersion('to', upgradeableVersions, nextVersion));

const from = `v${fromVersion}`;
const to = `v${toVersion}`;

Expand Down
21 changes: 21 additions & 0 deletions scripts/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type ExecSyncOptions, exec as execRaw } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { promisify } from 'node:util';

Expand Down Expand Up @@ -36,3 +37,23 @@ export const tempDirName = 'next-forge-update';
export const exec = promisify(execRaw);

export const supportedPackageManagers = ['npm', 'yarn', 'bun', 'pnpm'];

export const getAvailableVersions = async (): Promise<string[]> => {
const changelog = await readFile('CHANGELOG.md', 'utf-8');
const versionRegex = /# v(\d+\.\d+\.\d+)/g;
const matches = [...changelog.matchAll(versionRegex)];

return matches
.map((match) => match[1])
.sort((a, b) => {
const [aMajor, aMinor, aPatch] = a.split('.').map(Number);
const [bMajor, bMinor, bPatch] = b.split('.').map(Number);
if (aMajor !== bMajor) {
return bMajor - aMajor;
}
if (aMinor !== bMinor) {
return bMinor - aMinor;
}
return bPatch - aPatch;
});
};

0 comments on commit a8f3d83

Please sign in to comment.