astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
151 lines (150 loc) • 5.56 kB
JavaScript
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import * as clack from "@clack/prompts";
import ci from "ci-info";
import { detect, resolveCommand } from "package-manager-detector";
import colors from "piccolore";
import { exec } from "./exec.js";
const require = createRequire(import.meta.url);
const { bold, cyan, dim, magenta } = colors;
export async function getPackageVersion(packageName, cwd = process.cwd()) {
try {
const packageJsonPath = require.resolve(`${packageName}/package.json`, { paths: [cwd] });
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
return typeof packageJson.version === "string" ? packageJson.version : undefined;
} catch {
return undefined;
}
}
export async function getPackage(packageName, logger, options, otherDeps = []) {
try {
// Try to resolve with `createRequire` first to prevent ESM caching of the package
// if it errors and fails here
const resolved = require.resolve(packageName, { paths: [options.cwd ?? process.cwd()] });
const packageImport = await import(pathToFileURL(resolved).href);
return packageImport;
} catch {
if (options.optional) return undefined;
let message = `To continue, Astro requires the following dependency to be installed: ${bold(packageName)}.`;
if (ci.isCI) {
message += ` Packages cannot be installed automatically in CI environments.`;
}
logger.info("SKIP_FORMAT", message);
if (ci.isCI) {
return undefined;
}
const result = await installPackage([packageName, ...otherDeps], options, logger);
if (result) {
const resolved = require.resolve(packageName, { paths: [options.cwd ?? process.cwd()] });
const packageImport = await import(pathToFileURL(resolved).href);
return packageImport;
} else {
return undefined;
}
}
}
async function installPackage(packageNames, options, logger) {
const cwd = options.cwd ?? process.cwd();
const packageManager = await detect({
cwd,
// Include the `install-metadata` strategy to have the package manager that's
// used for installation take precedence
strategies: [
"install-metadata",
"lockfile",
"packageManager-field",
"devEngines-field"
]
});
const installCommand = resolveCommand(packageManager?.agent ?? "npm", "add", []);
if (!installCommand) return false;
if (installCommand.command === "deno") {
// Deno requires npm prefix to install packages
packageNames = packageNames.map((name) => `npm:${name}`);
}
const coloredOutput = `${bold(installCommand.command)} ${installCommand.args.join(" ")} ${cyan(packageNames.join(" "))}`;
logger.info("SKIP_FORMAT", `\n ${magenta("Astro will run the following command:")}\n ${dim("If you skip this step, you can always run it yourself later")}`);
clack.box(coloredOutput, undefined, {
rounded: true,
withGuide: false,
width: "auto"
});
let response;
if (options.skipAsk) {
response = true;
} else {
response = await clack.confirm({
message: colors.bold("Continue?"),
initialValue: true,
withGuide: false
}) === true;
}
if (Boolean(response)) {
const spinner = clack.spinner({ withGuide: false });
spinner.start("Installing dependencies...");
try {
await exec(installCommand.command, [...installCommand.args, ...packageNames], { nodeOptions: {
cwd,
// reset NODE_ENV to ensure install command run in dev mode
env: { NODE_ENV: undefined }
} });
spinner.stop("Dependencies installed.");
return true;
} catch (err) {
logger.debug("add", "Error installing dependencies", err);
spinner.error("Failed to install dependencies.");
return false;
}
} else {
return false;
}
}
export async function fetchPackageJson(scope, name, tag) {
const packageName = `${scope ? `${scope}/` : ""}${name}`;
const registry = await getRegistry();
const res = await fetch(`${registry}/${packageName}/${tag}`);
if (res.status >= 200 && res.status < 300) {
return await res.json();
} 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}/${tag} - GET ${res.status}`);
}
}
export async function fetchPackageVersions(packageName) {
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}`);
}
}
// 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 create-astro package
let _registry;
async function getRegistry() {
if (_registry) return _registry;
const fallback = "https://registry.npmjs.org";
const packageManager = (await detect())?.name || "npm";
try {
const { stdout } = await exec(packageManager, [
"config",
"get",
"registry"
]);
_registry = stdout.trim()?.replace(/\/$/, "") || fallback;
// Detect cases where the shell command returned a non-URL (e.g. a warning)
if (!new URL(_registry).host) _registry = fallback;
} catch {
_registry = fallback;
}
return _registry;
}