Compare commits
8 commits
main
...
spike/upgr
Author | SHA1 | Date | |
---|---|---|---|
|
bca381144e | ||
|
2a75c745eb | ||
|
cbb244421b | ||
|
ab01c48a26 | ||
|
7420004657 | ||
|
878bb78afe | ||
|
685637d6cb | ||
|
eb91b3ce1d |
12 changed files with 705 additions and 1 deletions
21
packages/upgrade/README.md
Normal file
21
packages/upgrade/README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# @astrojs/upgrade
|
||||
|
||||
## Upgrade utility for Astro projects
|
||||
|
||||
**With NPM:**
|
||||
|
||||
```bash
|
||||
npm exec @astrojs/upgrade
|
||||
```
|
||||
|
||||
**With Yarn:**
|
||||
|
||||
```bash
|
||||
yarn exec @astrojs/upgrade
|
||||
```
|
||||
|
||||
**With PNPM:**
|
||||
|
||||
```bash
|
||||
pnpm exec @astrojs/upgrade
|
||||
```
|
49
packages/upgrade/package.json
Normal file
49
packages/upgrade/package.json
Normal file
|
@ -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": {
|
||||
"@astrojs/upgrade": "./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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
}
|
||||
}
|
53
packages/upgrade/src/actions/context.ts
Normal file
53
packages/upgrade/src/actions/context.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
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;
|
||||
isDevDependency?: boolean;
|
||||
isMajor?: boolean;
|
||||
}
|
||||
|
||||
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
packages/upgrade/src/actions/help.ts
Normal file
15
packages/upgrade/src/actions/help.ts
Normal file
|
@ -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.']
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
107
packages/upgrade/src/actions/install.ts
Normal file
107
packages/upgrade/src/actions/install.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import type { Context, PackageInfo } from './context.js';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { color } from '@astrojs/cli-kit';
|
||||
import { celebrations, done, error, info, log, spinner, success, upgrade, banner, title } from '../messages.js';
|
||||
import { shell } from '../shell.js';
|
||||
import { random, sleep } from '@astrojs/cli-kit/utils';
|
||||
|
||||
const pluralize = (n: number) => {
|
||||
if (n === 1) return `One package has`;
|
||||
return `Some packages have`;
|
||||
}
|
||||
|
||||
export async function install(
|
||||
ctx: Pick<Context, 'version' | 'packages' | 'packageManager' | 'prompt' | 'dryRun' | 'exit' | 'cwd'>
|
||||
) {
|
||||
await banner();
|
||||
log('')
|
||||
const { current, dependencies, devDependencies } = filterPackages(ctx);
|
||||
const toInstall = [...dependencies, ...devDependencies];
|
||||
for (const packageInfo of current) {
|
||||
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]) {
|
||||
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('WARN!'),
|
||||
message: `Continue? ${pluralize(majors.length)} breaking changes!`,
|
||||
initial: true,
|
||||
});
|
||||
if (!proceed) ctx.exit(0);
|
||||
}
|
||||
|
||||
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;
|
||||
if (currentVersion === targetVersion) {
|
||||
current.push(packageInfo);
|
||||
} else {
|
||||
const arr = isDevDependency ? devDependencies : dependencies;
|
||||
arr.push(packageInfo);
|
||||
}
|
||||
}
|
||||
return { current, dependencies, devDependencies }
|
||||
}
|
||||
|
||||
async function runInstallCommand(ctx: Pick<Context, 'cwd' | 'packageManager'>, 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: random(done),
|
||||
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 (e) {
|
||||
const packages = [...dependencies, ...devDependencies].map(({ name, targetVersion }) => `${name}@${targetVersion}`).join(' ')
|
||||
error(
|
||||
'error',
|
||||
`Dependencies failed to install, please run the following command manually:\n${color.bold(`${ctx.packageManager} install ${packages}`)}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureYarnLock({ cwd }: { cwd: string }) {
|
||||
const yarnLock = path.join(cwd, 'yarn.lock');
|
||||
if (fs.existsSync(yarnLock)) return;
|
||||
return fs.promises.writeFile(yarnLock, '', { encoding: 'utf-8' });
|
||||
}
|
128
packages/upgrade/src/actions/verify.ts
Normal file
128
packages/upgrade/src/actions/verify.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
import type { Context, PackageInfo } from './context.js';
|
||||
|
||||
import dns from 'node:dns/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { color } from '@astrojs/cli-kit';
|
||||
import { bannerAbort, error, getRegistry, info, log } from '../messages.js';
|
||||
import semverDiff from 'semver/functions/diff.js';
|
||||
import semverCoerce from 'semver/functions/coerce.js'
|
||||
|
||||
|
||||
export async function verify(
|
||||
ctx: Pick<Context, 'version' | 'packages' | 'cwd' | 'dryRun' | 'exit'>
|
||||
) {
|
||||
const registry = await getRegistry();
|
||||
|
||||
if (!ctx.dryRun) {
|
||||
const online = await isOnline(registry);
|
||||
if (!online) {
|
||||
bannerAbort();
|
||||
log('');
|
||||
error('error', `Unable to connect to the internet.`);
|
||||
ctx.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await verifyAstroProject(ctx);
|
||||
|
||||
const ok = await verifyVersions(ctx, registry);
|
||||
if (!ok) {
|
||||
bannerAbort();
|
||||
log('');
|
||||
error('error', `Version ${color.reset(ctx.version)} ${color.dim('could not be found!')}`);
|
||||
await info('check', 'https://github.com/withastro/astro/releases');
|
||||
ctx.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function isOnline(registry: string): Promise<boolean> {
|
||||
const { host } = new URL(registry);
|
||||
return dns.lookup(host).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
}
|
||||
|
||||
function safeJSONParse(value: string) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {}
|
||||
return {}
|
||||
}
|
||||
|
||||
async function verifyAstroProject(ctx: Pick<Context, 'cwd' | 'version' | 'packages'>) {
|
||||
const packageJson = new URL('./package.json', ctx.cwd);
|
||||
if (!existsSync(packageJson)) return false;
|
||||
const contents = await readFile(packageJson, { encoding: 'utf-8' });
|
||||
if (!contents.includes('astro')) return false;
|
||||
|
||||
const { dependencies = {}, devDependencies = {} } = safeJSONParse(contents)
|
||||
if (dependencies['astro'] === undefined && devDependencies['astro'] === undefined) return false;
|
||||
|
||||
// Side-effect! Persist dependency info to the shared context
|
||||
collectPackageInfo(ctx, dependencies, devDependencies);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isAstroPackage(name: string) {
|
||||
return name === 'astro' || name.startsWith('@astrojs/');
|
||||
}
|
||||
|
||||
function collectPackageInfo(ctx: Pick<Context, 'version' | 'packages'>, dependencies: Record<string, string>, devDependencies: Record<string, string>) {
|
||||
for (const [name, currentVersion] of Object.entries(dependencies)) {
|
||||
if (!isAstroPackage(name)) continue;
|
||||
ctx.packages.push({
|
||||
name,
|
||||
currentVersion,
|
||||
targetVersion: ctx.version,
|
||||
})
|
||||
}
|
||||
for (const [name, currentVersion] of Object.entries(devDependencies)) {
|
||||
if (!isAstroPackage(name)) continue;
|
||||
ctx.packages.push({
|
||||
name,
|
||||
currentVersion,
|
||||
targetVersion: ctx.version,
|
||||
isDevDependency: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyVersions(ctx: Pick<Context, 'version' | 'packages'>, registry: string) {
|
||||
const tasks: Promise<void>[] = [];
|
||||
for (const packageInfo of ctx.packages) {
|
||||
tasks.push(resolveTargetVersion(packageInfo, registry));
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
for (const packageInfo of ctx.packages) {
|
||||
if (!packageInfo.targetVersion) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function resolveTargetVersion(packageInfo: PackageInfo, registry: string): Promise<void> {
|
||||
const res = await fetch(`${registry}/${packageInfo.name}/${packageInfo.targetVersion}`);
|
||||
const { code, version } = await res.json()
|
||||
if (code === 'ResourceNotFound') {
|
||||
if (packageInfo.targetVersion === 'latest') {
|
||||
packageInfo.targetVersion = '';
|
||||
return;
|
||||
} else {
|
||||
return resolveTargetVersion({ ...packageInfo, targetVersion: 'latest' }, registry);
|
||||
}
|
||||
}
|
||||
if (packageInfo.currentVersion === version) {
|
||||
return;
|
||||
}
|
||||
const prefix = packageInfo.targetVersion === 'latest' ? '^' : '';
|
||||
packageInfo.targetVersion = `${prefix}${version}`;
|
||||
const fromVersion = semverCoerce(packageInfo.currentVersion)!;
|
||||
const toVersion = semverCoerce(packageInfo.targetVersion)!;
|
||||
const bump = semverDiff(fromVersion, toVersion);
|
||||
packageInfo.isMajor = bump === 'major';
|
||||
}
|
||||
|
40
packages/upgrade/src/index.ts
Normal file
40
packages/upgrade/src/index.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { getContext } from './actions/context.js';
|
||||
|
||||
import { install } from './actions/install.js';
|
||||
import { help } from './actions/help.js';
|
||||
import { verify } from './actions/verify.js';
|
||||
import { setStdout } from './messages.js';
|
||||
|
||||
const exit = () => process.exit(0);
|
||||
process.on('SIGINT', exit);
|
||||
process.on('SIGTERM', exit);
|
||||
|
||||
export async function main() {
|
||||
// NOTE: In the v7.x version of npm, the default behavior of `npm init` was changed
|
||||
// to no longer require `--` to pass args and instead pass `--` directly to us. This
|
||||
// broke our arg parser, since `--` is a special kind of flag. Filtering for `--` here
|
||||
// fixes the issue so that create-astro now works on all npm versions.
|
||||
const cleanArgv = process.argv.slice(2).filter((arg) => arg !== '--');
|
||||
const ctx = await getContext(cleanArgv);
|
||||
if (ctx.help) {
|
||||
help();
|
||||
return;
|
||||
}
|
||||
|
||||
const steps = [
|
||||
verify,
|
||||
install,
|
||||
];
|
||||
|
||||
for (const step of steps) {
|
||||
await step(ctx);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
export {
|
||||
install,
|
||||
getContext,
|
||||
setStdout,
|
||||
verify,
|
||||
};
|
169
packages/upgrade/src/messages.ts
Normal file
169
packages/upgrade/src/messages.ts
Normal file
|
@ -0,0 +1,169 @@
|
|||
/* eslint no-console: 'off' */
|
||||
import type { PackageInfo } from './actions/context.js';
|
||||
import { color, label, spinner as load } from '@astrojs/cli-kit';
|
||||
import { align } from '@astrojs/cli-kit/utils';
|
||||
import detectPackageManager from 'which-pm-runs';
|
||||
import { shell } from './shell.js';
|
||||
import semverCoerce from 'semver/functions/coerce.js';
|
||||
|
||||
// Users might lack access to the global npm registry, this function
|
||||
// checks the user's project type and will return the proper npm registry
|
||||
//
|
||||
// A copy of this function also exists in the astro package
|
||||
export async function getRegistry(): Promise<string> {
|
||||
const packageManager = detectPackageManager()?.name || 'npm';
|
||||
try {
|
||||
const { stdout } = await shell(packageManager, ['config', 'get', 'registry']);
|
||||
return stdout?.trim()?.replace(/\/$/, '') || 'https://registry.npmjs.org';
|
||||
} catch (e) {
|
||||
return 'https://registry.npmjs.org';
|
||||
}
|
||||
}
|
||||
|
||||
let stdout = process.stdout;
|
||||
/** @internal Used to mock `process.stdout.write` for testing purposes */
|
||||
export function setStdout(writable: typeof process.stdout) {
|
||||
stdout = writable;
|
||||
}
|
||||
|
||||
export async function spinner(args: {
|
||||
start: string;
|
||||
end: string;
|
||||
while: (...args: any) => Promise<any>;
|
||||
}) {
|
||||
await load(args, { stdout });
|
||||
}
|
||||
|
||||
export const celebrations = [
|
||||
'Beautiful.',
|
||||
'Excellent!',
|
||||
'Sweet!',
|
||||
'Nice!',
|
||||
'Huzzah!',
|
||||
'Success.',
|
||||
'Nice.',
|
||||
'Wonderful.',
|
||||
'Lovely!',
|
||||
'Lookin\' good.',
|
||||
'Awesome.'
|
||||
]
|
||||
|
||||
export const done = [
|
||||
'You\'re on the latest and greatest.',
|
||||
'Everything is current.',
|
||||
'Integrations are all up to date.',
|
||||
'All done. Thanks for using Astro!',
|
||||
'Integrations up to date. Enjoy building!',
|
||||
'All set, everything is up to date.',
|
||||
]
|
||||
|
||||
export const log = (message: string) => stdout.write(message + '\n');
|
||||
export const banner = async () =>
|
||||
log(
|
||||
`\n${label('astro', color.bgGreen, color.black)} ${color.bold('Integration upgrade in progress.')}`
|
||||
);
|
||||
|
||||
export const bannerAbort = () =>
|
||||
log(`\n${label('astro', color.bgRed)} ${color.bold('Integration upgrade aborted.')}`);
|
||||
|
||||
export const info = async (prefix: string, text: string, version = '') => {
|
||||
const length = 11 + prefix.length + text.length + version?.length;
|
||||
const symbol = '◼';
|
||||
if (length > stdout.columns) {
|
||||
log(`${' '.repeat(5)} ${color.cyan(symbol)} ${prefix}`);
|
||||
log(`${' '.repeat(9)}${color.dim(text)} ${color.reset(version)}`);
|
||||
} else {
|
||||
log(`${' '.repeat(5)} ${color.cyan(symbol)} ${prefix} ${color.dim(text)} ${color.reset(version)}`);
|
||||
}
|
||||
}
|
||||
export const upgrade = async (packageInfo: PackageInfo, text: string) => {
|
||||
const { name, isMajor = false, targetVersion } = packageInfo;
|
||||
|
||||
const bg = isMajor ? (v: string) => color.bgYellow(color.black(` ${v} `)) : color.green;
|
||||
const style = isMajor ? color.yellow : color.green;
|
||||
const symbol = isMajor ? '▲' : '●';
|
||||
const toVersion = semverCoerce(targetVersion)!;
|
||||
const version = `v${toVersion.version}`;
|
||||
|
||||
const length = 12 + name.length + text.length + version.length;
|
||||
if (length > stdout.columns) {
|
||||
log(`${' '.repeat(5)} ${style(symbol)} ${name}`);
|
||||
log(`${' '.repeat(9)}${color.dim(text)} ${bg(version)}`);
|
||||
} else {
|
||||
log(`${' '.repeat(5)} ${style(symbol)} ${name} ${color.dim(text)} ${bg(version)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const title = (text: string) => align(label(text, color.bgYellow, color.black), 'end', 7) + ' ';
|
||||
|
||||
export const success = async (prefix: string, text: string) => {
|
||||
const length = 10 + prefix.length + text.length;
|
||||
if (length > stdout.columns) {
|
||||
log(`${' '.repeat(5)} ${color.green("✔")} ${prefix}`);
|
||||
log(`${' '.repeat(9)}${color.dim(text)}`);
|
||||
} else {
|
||||
log(`${' '.repeat(5)} ${color.green("✔")} ${prefix} ${color.dim(text)}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const error = async (prefix: string, text: string) => {
|
||||
if (stdout.columns < 80) {
|
||||
log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)}`);
|
||||
log(`${' '.repeat(9)}${color.dim(text)}`);
|
||||
} else {
|
||||
log(`${' '.repeat(5)} ${color.red('▲')} ${color.red(prefix)} ${color.dim(text)}`);
|
||||
}
|
||||
};
|
||||
|
||||
export function printHelp({
|
||||
commandName,
|
||||
usage,
|
||||
tables,
|
||||
description,
|
||||
}: {
|
||||
commandName: string;
|
||||
headline?: string;
|
||||
usage?: string;
|
||||
tables?: Record<string, [command: string, help: string][]>;
|
||||
description?: string;
|
||||
}) {
|
||||
const linebreak = () => '';
|
||||
const table = (rows: [string, string][], { padding }: { padding: number }) => {
|
||||
const split = stdout.columns < 60;
|
||||
let raw = '';
|
||||
|
||||
for (const row of rows) {
|
||||
if (split) {
|
||||
raw += ` ${row[0]}\n `;
|
||||
} else {
|
||||
raw += `${`${row[0]}`.padStart(padding)}`;
|
||||
}
|
||||
raw += ' ' + color.dim(row[1]) + '\n';
|
||||
}
|
||||
|
||||
return raw.slice(0, -1); // remove latest \n
|
||||
};
|
||||
|
||||
let message = [];
|
||||
|
||||
if (usage) {
|
||||
message.push(linebreak(), `${color.green(commandName)} ${color.bold(usage)}`);
|
||||
}
|
||||
|
||||
if (tables) {
|
||||
function calculateTablePadding(rows: [string, string][]) {
|
||||
return rows.reduce((val, [first]) => Math.max(val, first.length), 0);
|
||||
}
|
||||
const tableEntries = Object.entries(tables);
|
||||
const padding = Math.max(...tableEntries.map(([, rows]) => calculateTablePadding(rows)));
|
||||
for (const [, tableRows] of tableEntries) {
|
||||
message.push(linebreak(), table(tableRows, { padding }));
|
||||
}
|
||||
}
|
||||
|
||||
if (description) {
|
||||
message.push(linebreak(), `${description}`);
|
||||
}
|
||||
|
||||
log(message.join('\n') + '\n');
|
||||
}
|
51
packages/upgrade/src/shell.ts
Normal file
51
packages/upgrade/src/shell.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
// This is an extremely simplified version of [`execa`](https://github.com/sindresorhus/execa)
|
||||
// intended to keep our dependency size down
|
||||
import type { ChildProcess, StdioOptions } from 'node:child_process';
|
||||
import type { Readable } from 'node:stream';
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { text as textFromStream } from 'node:stream/consumers';
|
||||
|
||||
export interface ExecaOptions {
|
||||
cwd?: string | URL;
|
||||
stdio?: StdioOptions;
|
||||
timeout?: number;
|
||||
}
|
||||
export interface Output {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
}
|
||||
const text = (stream: NodeJS.ReadableStream | Readable | null) =>
|
||||
stream ? textFromStream(stream).then((t) => t.trimEnd()) : '';
|
||||
|
||||
export async function shell(
|
||||
command: string,
|
||||
flags: string[],
|
||||
opts: ExecaOptions = {}
|
||||
): Promise<Output> {
|
||||
let child: ChildProcess;
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
try {
|
||||
child = spawn(command, flags, {
|
||||
cwd: opts.cwd,
|
||||
shell: true,
|
||||
stdio: opts.stdio,
|
||||
timeout: opts.timeout,
|
||||
});
|
||||
const done = new Promise((resolve) => child.on('close', resolve));
|
||||
[stdout, stderr] = await Promise.all([text(child.stdout), text(child.stderr)]);
|
||||
await done;
|
||||
} catch (e) {
|
||||
throw { stdout, stderr, exitCode: 1 };
|
||||
}
|
||||
const { exitCode } = child;
|
||||
if (exitCode === null) {
|
||||
throw new Error('Timeout');
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
return { stdout, stderr, exitCode };
|
||||
}
|
13
packages/upgrade/tsconfig.json
Normal file
13
packages/upgrade/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src", "index.d.ts"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"outDir": "./dist",
|
||||
"declarationDir": "./dist/types"
|
||||
}
|
||||
}
|
15
packages/upgrade/upgrade.mjs
Executable file
15
packages/upgrade/upgrade.mjs
Executable file
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
'use strict';
|
||||
|
||||
const currentVersion = process.versions.node;
|
||||
const requiredMajorVersion = parseInt(currentVersion.split('.')[0], 10);
|
||||
const minimumMajorVersion = 18;
|
||||
|
||||
if (requiredMajorVersion < minimumMajorVersion) {
|
||||
console.error(`Node.js v${currentVersion} is out of date and unsupported!`);
|
||||
console.error(`Please use Node.js v${minimumMajorVersion} or higher.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import('./dist/index.js').then(({ main }) => main());
|
|
@ -5083,6 +5083,37 @@ importers:
|
|||
specifier: ^10.2.0
|
||||
version: 10.2.0
|
||||
|
||||
packages/upgrade:
|
||||
dependencies:
|
||||
'@astrojs/cli-kit':
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
semver:
|
||||
specifier: ^7.5.4
|
||||
version: 7.5.4
|
||||
which-pm-runs:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
devDependencies:
|
||||
'@types/semver':
|
||||
specifier: ^7.5.2
|
||||
version: 7.5.2
|
||||
'@types/which-pm-runs':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
arg:
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
astro-scripts:
|
||||
specifier: workspace:*
|
||||
version: link:../../scripts
|
||||
chai:
|
||||
specifier: ^4.3.7
|
||||
version: 4.3.7
|
||||
mocha:
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0
|
||||
|
||||
scripts:
|
||||
dependencies:
|
||||
arg:
|
||||
|
@ -5218,6 +5249,14 @@ packages:
|
|||
- prettier-plugin-astro
|
||||
dev: true
|
||||
|
||||
/@astrojs/cli-kit@0.2.3:
|
||||
resolution: {integrity: sha512-MjB42mpIG/F2rFtdp4f3NylFCILuFSib2yITSq65fRaDFn8+UC8EMh6T7Jr3YqHAbUY5r8V8QWNgH4keOEO2BA==}
|
||||
dependencies:
|
||||
chalk: 5.3.0
|
||||
log-update: 5.0.1
|
||||
sisteransi: 1.0.5
|
||||
dev: false
|
||||
|
||||
/@astrojs/cli-kit@0.3.0:
|
||||
resolution: {integrity: sha512-nil0Kz2xuzR3xQX+FVHg2W8g+FvbeUeoCeU53duQjAFuHRJrbqWRmgfjYeM6f2780dsSuGiYMXmY+IaJqaqiaw==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
|
@ -9025,6 +9064,10 @@ packages:
|
|||
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
|
||||
dev: true
|
||||
|
||||
/@types/semver@7.5.2:
|
||||
resolution: {integrity: sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==}
|
||||
dev: true
|
||||
|
||||
/@types/send@0.17.1:
|
||||
resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==}
|
||||
dependencies:
|
||||
|
@ -10565,7 +10608,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/concat-map@0.0.1:
|
||||
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
/console-control-strings@1.1.0:
|
||||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||
|
|
Loading…
Reference in a new issue