diff --git a/packages/upgrade/README.md b/packages/upgrade/README.md new file mode 100644 index 000000000..0f9db27a7 --- /dev/null +++ b/packages/upgrade/README.md @@ -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 +``` diff --git a/packages/upgrade/package.json b/packages/upgrade/package.json new file mode 100644 index 000000000..3b4388eec --- /dev/null +++ b/packages/upgrade/package.json @@ -0,0 +1,48 @@ +{ + "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", + "node-fetch-native": "^1.4.0", + "which-pm-runs": "^1.1.0" + }, + "devDependencies": { + "@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" + } +} diff --git a/packages/upgrade/src/actions/context.ts b/packages/upgrade/src/actions/context.ts new file mode 100644 index 000000000..d58986f7d --- /dev/null +++ b/packages/upgrade/src/actions/context.ts @@ -0,0 +1,52 @@ +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; +} + +export async function getContext(argv: string[]): Promise { + 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: pathToFileURL(process.cwd()), + dryRun, + version, + exit(code) { + process.exit(code); + }, + } satisfies Context +} diff --git a/packages/upgrade/src/actions/help.ts b/packages/upgrade/src/actions/help.ts new file mode 100644 index 000000000..d61abc71e --- /dev/null +++ b/packages/upgrade/src/actions/help.ts @@ -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.'] + ], + }, + }); +} diff --git a/packages/upgrade/src/actions/install.ts b/packages/upgrade/src/actions/install.ts new file mode 100644 index 000000000..6e14a2663 --- /dev/null +++ b/packages/upgrade/src/actions/install.ts @@ -0,0 +1,57 @@ +import type { Context } 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 { error, info, spinner } from '../messages.js'; +import { shell } from '../shell.js'; + +export async function install( + ctx: Pick +) { + if (ctx.dryRun) { + await info('--dry-run', `Skipping dependency installation`); + } else { + await spinner({ + start: `Updating dependencies with ${ctx.packageManager}...`, + end: 'Dependencies updated', + while: () => { + return runCommand(ctx).catch((e) => { + error('error', e); + error( + 'error', + `Dependencies failed to install, please run ${color.bold( + ctx.packageManager + ' install' + )} to install them manually after setup.` + ); + }); + }, + }); + } +} + +async function runCommand(ctx: Pick) { + const cwd = fileURLToPath(ctx.cwd); + if (ctx.packageManager === 'yarn') await ensureYarnLock({ cwd }); + + const dependencies: string[] = []; + const devDependencies: string[] = []; + for (const { name, targetVersion, isDevDependency } of ctx.packages) { + const arr = isDevDependency ? devDependencies : dependencies; + arr.push(`${name}@${targetVersion}`); + } + + if (dependencies.length > 0) { + await shell(ctx.packageManager, ['install', ...dependencies], { cwd, timeout: 90_000, stdio: 'ignore' }); + } + if (devDependencies.length > 0) { + await shell(ctx.packageManager, ['install', '--save-dev', ...devDependencies], { cwd, timeout: 90_000, stdio: 'ignore' }); + } +} + +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' }); +} diff --git a/packages/upgrade/src/actions/verify.ts b/packages/upgrade/src/actions/verify.ts new file mode 100644 index 000000000..2f7d65443 --- /dev/null +++ b/packages/upgrade/src/actions/verify.ts @@ -0,0 +1,109 @@ +import type { Context, PackageInfo } from './context'; + +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'; + + +export async function verify( + ctx: Pick +) { + 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 { + 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) { + 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, dependencies: Record, devDependencies: Record) { + 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, registry: string) { + const tasks: Promise[] = []; + for (const packageInfo of ctx.packages) { + tasks.push(resolveTargetVersion(packageInfo, registry)); + } + await Promise.all(tasks); + for (const packageInfo of ctx.packages) { + // check for packageInfo.targetVersion, then fallback + // check if packageInfo.targetVersion === packageInfo.currentVersion + } + return true; +} + +async function resolveTargetVersion(packageInfo: PackageInfo, registry: string): Promise { + const res = await fetch(`${registry}/${packageInfo.name}/tags/${packageInfo.targetVersion}`); + + // MUTATE packageInfo.targetVersion to resolvedValue +} + diff --git a/packages/upgrade/src/index.ts b/packages/upgrade/src/index.ts new file mode 100644 index 000000000..ab216ad56 --- /dev/null +++ b/packages/upgrade/src/index.ts @@ -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, +}; diff --git a/packages/upgrade/src/messages.ts b/packages/upgrade/src/messages.ts new file mode 100644 index 000000000..1dc0711de --- /dev/null +++ b/packages/upgrade/src/messages.ts @@ -0,0 +1,221 @@ +/* eslint no-console: 'off' */ +import { color, say as houston, label, spinner as load } from '@astrojs/cli-kit'; +import { align, sleep } from '@astrojs/cli-kit/utils'; +import fetch from 'node-fetch-native'; +import { exec } from 'node:child_process'; +import stripAnsi from 'strip-ansi'; +import detectPackageManager from 'which-pm-runs'; +import { shell } from './shell.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 { + 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 say(messages: string | string[], { clear = false, hat = '' } = {}) { + return houston(messages, { clear, hat, stdout }); +} + +export async function spinner(args: { + start: string; + end: string; + while: (...args: any) => Promise; +}) { + await load(args, { stdout }); +} + +export const title = (text: string) => align(label(text), 'end', 7) + ' '; + +export const welcome = [ + `Let's claim your corner of the internet.`, + `I'll be your assistant today.`, + `Let's build something awesome!`, + `Let's build something great!`, + `Let's build something fast!`, + `Let's build the web we want.`, + `Let's make the web weird!`, + `Let's make the web a better place!`, + `Let's create a new project!`, + `Let's create something unique!`, + `Time to build a new website.`, + `Time to build a faster website.`, + `Time to build a sweet new website.`, + `We're glad to have you on board.`, + `Keeping the internet weird since 2021.`, + `Initiating launch sequence...`, + `Initiating launch sequence... right... now!`, + `Awaiting further instructions.`, +]; + +export const getName = () => + new Promise((resolve) => { + exec('git config user.name', { encoding: 'utf-8' }, (_1, gitName) => { + if (gitName.trim()) { + return resolve(gitName.split(' ')[0].trim()); + } + exec('whoami', { encoding: 'utf-8' }, (_3, whoami) => { + if (whoami.trim()) { + return resolve(whoami.split(' ')[0].trim()); + } + return resolve('astronaut'); + }); + }); + }); + +let v: string; +export const getVersion = () => + new Promise(async (resolve) => { + if (v) return resolve(v); + let registry = await getRegistry(); + const { version } = await fetch(`${registry}/astro/latest`, { redirect: 'follow' }).then( + (res) => res.json(), + () => ({ version: '' }) + ); + v = version; + resolve(version); + }); + +export const log = (message: string) => stdout.write(message + '\n'); +export const banner = async (version: string) => + log( + `\n${label('astro', color.bgGreen, color.black)}${ + version ? ' ' + color.green(color.bold(`v${version}`)) : '' + } ${color.bold('Launch sequence initiated.')}` + ); + +export const bannerAbort = () => + log(`\n${label('astro', color.bgRed)} ${color.bold('Launch sequence aborted.')}`); + +export const info = async (prefix: string, text: string) => { + await sleep(100); + if (stdout.columns < 80) { + log(`${' '.repeat(5)} ${color.cyan('◼')} ${color.cyan(prefix)}`); + log(`${' '.repeat(9)}${color.dim(text)}`); + } else { + log(`${' '.repeat(5)} ${color.cyan('◼')} ${color.cyan(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 const typescriptByDefault = async () => { + await info(`No worries!`, 'TypeScript is supported in Astro by default,'); + log(`${' '.repeat(9)}${color.dim('but you are free to continue writing JavaScript instead.')}`); + await sleep(1000); +}; + +export const nextSteps = async ({ projectDir, devCmd }: { projectDir: string; devCmd: string }) => { + const max = stdout.columns; + const prefix = max < 80 ? ' ' : ' '.repeat(9); + await sleep(200); + log( + `\n ${color.bgCyan(` ${color.black('next')} `)} ${color.bold( + 'Liftoff confirmed. Explore your project!' + )}` + ); + + await sleep(100); + if (projectDir !== '') { + projectDir = projectDir.includes(' ') ? `"./${projectDir}"` : `./${projectDir}`; + const enter = [ + `\n${prefix}Enter your project directory using`, + color.cyan(`cd ${projectDir}`, ''), + ]; + const len = enter[0].length + stripAnsi(enter[1]).length; + log(enter.join(len > max ? '\n' + prefix : ' ')); + } + log( + `${prefix}Run ${color.cyan(devCmd)} to start the dev server. ${color.cyan('CTRL+C')} to stop.` + ); + await sleep(100); + log( + `${prefix}Add frameworks like ${color.cyan(`react`)} or ${color.cyan( + 'tailwind' + )} using ${color.cyan('astro add')}.` + ); + await sleep(100); + log(`\n${prefix}Stuck? Join us at ${color.cyan(`https://astro.build/chat`)}`); + await sleep(200); +}; + +export function printHelp({ + commandName, + headline, + usage, + tables, + description, +}: { + commandName: string; + headline?: string; + usage?: string; + tables?: Record; + 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 (headline) { + message.push( + linebreak(), + `${title(commandName)} ${color.green(`v${process.env.PACKAGE_VERSION ?? ''}`)} ${headline}` + ); + } + + 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'); +} diff --git a/packages/upgrade/src/shell.ts b/packages/upgrade/src/shell.ts new file mode 100644 index 000000000..e6f1295ea --- /dev/null +++ b/packages/upgrade/src/shell.ts @@ -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 { + 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 }; +} diff --git a/packages/upgrade/test/fixtures/empty/.gitkeep b/packages/upgrade/test/fixtures/empty/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/upgrade/test/fixtures/not-empty/git.json b/packages/upgrade/test/fixtures/not-empty/git.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/packages/upgrade/test/fixtures/not-empty/git.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/upgrade/test/fixtures/not-empty/package.json b/packages/upgrade/test/fixtures/not-empty/package.json new file mode 100644 index 000000000..5edb64fea --- /dev/null +++ b/packages/upgrade/test/fixtures/not-empty/package.json @@ -0,0 +1,4 @@ +{ + "name": "@test/create-astro-not-empty", + "private": true +} diff --git a/packages/upgrade/test/fixtures/not-empty/tsconfig.json b/packages/upgrade/test/fixtures/not-empty/tsconfig.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/packages/upgrade/test/fixtures/not-empty/tsconfig.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/upgrade/test/install.test.js b/packages/upgrade/test/install.test.js new file mode 100644 index 000000000..ca1224aa0 --- /dev/null +++ b/packages/upgrade/test/install.test.js @@ -0,0 +1,72 @@ +import { expect } from 'chai'; + +import { install } from '../dist/index.js'; +import { setup } from './utils.js'; + +describe('install', () => { + const fixture = setup(); + + it('--yes', async () => { + const context = { + cwd: '', + yes: true, + pkgManager: 'npm', + dryRun: true, + prompt: () => ({ deps: true }), + }; + await install(context); + expect(fixture.hasMessage('Skipping dependency installation')).to.be.true; + }); + + it('prompt yes', async () => { + const context = { + cwd: '', + pkgManager: 'npm', + dryRun: true, + prompt: () => ({ deps: true }), + install: undefined, + }; + await install(context); + expect(fixture.hasMessage('Skipping dependency installation')).to.be.true; + expect(context.install).to.eq(true); + }); + + it('prompt no', async () => { + const context = { + cwd: '', + pkgManager: 'npm', + dryRun: true, + prompt: () => ({ deps: false }), + install: undefined, + }; + await install(context); + expect(fixture.hasMessage('Skipping dependency installation')).to.be.true; + expect(context.install).to.eq(false); + }); + + it('--install', async () => { + const context = { + cwd: '', + install: true, + pkgManager: 'npm', + dryRun: true, + prompt: () => ({ deps: false }), + }; + await install(context); + expect(fixture.hasMessage('Skipping dependency installation')).to.be.true; + expect(context.install).to.eq(true); + }); + + it('--no-install', async () => { + const context = { + cwd: '', + install: false, + pkgManager: 'npm', + dryRun: true, + prompt: () => ({ deps: false }), + }; + await install(context); + expect(fixture.hasMessage('Skipping dependency installation')).to.be.true; + expect(context.install).to.eq(false); + }); +}); diff --git a/packages/upgrade/test/utils.js b/packages/upgrade/test/utils.js new file mode 100644 index 000000000..ff5d5dd83 --- /dev/null +++ b/packages/upgrade/test/utils.js @@ -0,0 +1,31 @@ +import { setStdout } from '../dist/index.js'; +import stripAnsi from 'strip-ansi'; + +export function setup() { + const ctx = { messages: [] }; + before(() => { + setStdout( + Object.assign({}, process.stdout, { + write(buf) { + ctx.messages.push(stripAnsi(String(buf)).trim()); + return true; + }, + }) + ); + }); + beforeEach(() => { + ctx.messages = []; + }); + + return { + messages() { + return ctx.messages; + }, + length() { + return ctx.messages.length; + }, + hasMessage(content) { + return !!ctx.messages.find((msg) => msg.includes(content)); + }, + }; +} diff --git a/packages/upgrade/test/verify.test.js b/packages/upgrade/test/verify.test.js new file mode 100644 index 000000000..ecfaba727 --- /dev/null +++ b/packages/upgrade/test/verify.test.js @@ -0,0 +1,41 @@ +import { expect } from 'chai'; + +import { verify } from '../dist/index.js'; +import { setup } from './utils.js'; + +describe('verify', () => { + const fixture = setup(); + const exit = (code) => { + throw code; + }; + + it('basics', async () => { + const context = { template: 'basics', exit }; + await verify(context); + expect(fixture.messages().length).to.equal(0, 'Did not expect `verify` to log any messages'); + }); + + it('missing', async () => { + const context = { template: 'missing', exit }; + let err = null; + try { + await verify(context); + } catch (e) { + err = e; + } + expect(err).to.eq(1); + expect(fixture.hasMessage('Template missing does not exist!')); + }); + + it('starlight', async () => { + const context = { template: 'starlight', exit }; + await verify(context); + expect(fixture.messages().length).to.equal(0, 'Did not expect `verify` to log any messages'); + }); + + it('starlight/tailwind', async () => { + const context = { template: 'starlight/tailwind', exit }; + await verify(context); + expect(fixture.messages().length).to.equal(0, 'Did not expect `verify` to log any messages'); + }); +}); diff --git a/packages/upgrade/tsconfig.json b/packages/upgrade/tsconfig.json new file mode 100644 index 000000000..1ab34c5a2 --- /dev/null +++ b/packages/upgrade/tsconfig.json @@ -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" + } +} diff --git a/packages/upgrade/upgrade.mjs b/packages/upgrade/upgrade.mjs new file mode 100755 index 000000000..f9df779d8 --- /dev/null +++ b/packages/upgrade/upgrade.mjs @@ -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());