UNPKG

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
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; }