Skip to content

Commit

Permalink
Ensure astro add only installs stable versions (#9387)
Browse files Browse the repository at this point in the history
* fix(add): update peerDependency resolution logic to exclude prereleases

* chore: add changeset
  • Loading branch information
natemoo-re authored Dec 11, 2023
1 parent ea09182 commit a7c75b3
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 11 deletions.
11 changes: 11 additions & 0 deletions .changeset/young-rats-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'astro': patch
---

Fixes an edge case with `astro add` that could install a prerelease instead of a stable release version.

**Prior to this change**
`astro add svelte` installs `svelte@5.0.0-next.22`

**After this change**
`astro add svelte` installs `svelte@4.2.8`
1 change: 1 addition & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@
"@types/probe-image-size": "^7.2.3",
"@types/prompts": "^2.4.8",
"@types/resolve": "^1.20.5",
"@types/semver": "^7.5.2",
"@types/send": "^0.17.4",
"@types/server-destroy": "^1.0.3",
"@types/unist": "^3.0.2",
Expand Down
57 changes: 47 additions & 10 deletions packages/astro/src/cli/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { bold, cyan, dim, green, magenta, red, yellow } from 'kleur/colors';
import fsMod, { existsSync, promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import maxSatisfying from 'semver/ranges/max-satisfying.js';
import ora from 'ora';
import preferredPM from 'preferred-pm';
import prompts from 'prompts';
Expand Down Expand Up @@ -610,15 +611,7 @@ async function getInstallIntegrationsCommand({
logger.debug('add', `package manager: ${JSON.stringify(pm)}`);
if (!pm) return null;

let dependencies = integrations
.map<[string, string | null][]>((i) => [[i.packageName, null], ...i.dependencies])
.flat(1)
.filter((dep, i, arr) => arr.findIndex((d) => d[0] === dep[0]) === i)
.map(([name, version]) =>
version === null ? name : `${name}@${version.split(/\s*\|\|\s*/).pop()}`
)
.sort();

const dependencies = await convertIntegrationsToInstallSpecifiers(integrations);
switch (pm.name) {
case 'npm':
return { pm: 'npm', command: 'install', flags: [], dependencies };
Expand All @@ -633,6 +626,35 @@ async function getInstallIntegrationsCommand({
}
}

async function convertIntegrationsToInstallSpecifiers(
integrations: IntegrationInfo[]
): Promise<string[]> {
const ranges: Record<string, string> = {};
for (let { packageName, dependencies } of integrations) {
ranges[packageName] = '*';
for (const [name, range] of dependencies) {
ranges[name] = range;
}
}
return Promise.all(
Object.entries(ranges).map(([name, range]) => resolveRangeToInstallSpecifier(name, range))
);
}

/**
* Resolves package with a given range to a STABLE version
* peerDependencies might specify a compatible prerelease,
* but `astro add` should only ever install stable releases
*/
async function resolveRangeToInstallSpecifier(name: string, range: string): Promise<string> {
const versions = await fetchPackageVersions(name);
if (versions instanceof Error) return name;
// Filter out any prerelease versions
const stableVersions = versions.filter(v => !v.includes('-'));
const maxStable = maxSatisfying(stableVersions, range);
return `${name}@^${maxStable}`;
}

// Allow forwarding of standard `npm install` flags
// See https://docs.npmjs.com/cli/v8/commands/npm-install#description
const INHERITED_FLAGS = new Set<string>([
Expand Down Expand Up @@ -725,7 +747,7 @@ async function fetchPackageJson(
scope: string | undefined,
name: string,
tag: string
): Promise<object | Error> {
): Promise<Record<string, any> | Error> {
const packageName = `${scope ? `${scope}/` : ''}${name}`;
const registry = await getRegistry();
const res = await fetch(`${registry}/${packageName}/${tag}`);
Expand All @@ -739,6 +761,21 @@ async function fetchPackageJson(
}
}

async function fetchPackageVersions(packageName: string): Promise<string[] | Error> {
const registry = await getRegistry();
const res = await fetch(`${registry}/${packageName}`, {
headers: { accept: 'application/vnd.npm.install-v1+json' },
});
if (res.status >= 200 && res.status < 300) {
return await res.json().then((data) => Object.keys(data.versions));
} else if (res.status === 404) {
// 404 means the package doesn't exist, so we don't need an error message here
return new Error();
} else {
return new Error(`Failed to fetch ${registry}/${packageName} - GET ${res.status}`);
}
}

export async function validateIntegrations(integrations: string[]): Promise<IntegrationInfo[]> {
const spinner = ora('Resolving packages...').start();
try {
Expand Down
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a7c75b3

Please sign in to comment.