UNPKG

create-compas

Version:

Create compas based applications

232 lines (202 loc) 6.39 kB
import { createWriteStream } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import https from "node:https"; import os from "node:os"; import { normalize } from "node:path"; import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; import { AppError, environment, exec, pathJoin, spawn, uuid, } from "@compas/stdlib"; import tar from "tar"; /** * Try to resolve the template, this way we can explicitly error instead of an extraction * error because of a 40x response. * * @param {import("@compas/stdlib").Logger} logger * @param {import("./arg-parser.js").CreateCompasArgs} options * @returns {Promise<void>} */ export async function templateCheckIfExists(logger, options) { if (options.help) { return; } const errorMessage = `The template could not be resolved, see 'create-compas --help'. If you used a Compas provided template, make sure it exists at 'https://github.com/compasjs/compas/tree/${ options.template.ref ?? "main" }/examples/'.`; if (options.template.provider === "github") { try { await new Promise((resolve, reject) => { const req = https.request( `https://github.com/${options.template.repository}/blob/${ options.template.ref ? options.template.ref : "-" }/${ options.template.path ? `${options.template.path}/package.json` : "package.json" }`, { method: "HEAD", }, (res) => { res.on("error", reject); if ((res?.statusCode ?? 500) > 399) { reject(new Error()); } else { // @ts-expect-error resolve(); } }, ); req.on("error", reject); req.end(); }); } catch { logger.error(errorMessage); return process.exit(1); } } else { logger.error(errorMessage); return process.exit(1); } } /** * Download and extract the template repository. * Does not do any post-processing. * * @param {import("@compas/stdlib").Logger} logger * @param {import("./arg-parser.js").CreateCompasArgs} options * @returns {Promise<void>} */ export async function templateGetAndExtractStream(logger, options) { if (options.help) { return; } await mkdir(options.outputDirectory, { recursive: true }); const tmpFile = pathJoin(os.tmpdir(), `create-compas-${uuid()}`); let httpStream = Readable.from(Buffer.from("")); if (options.template.provider === "github") { logger.info(`Resolving remote template...`); // @ts-expect-error httpStream = await templateGetHttpStream( `https://codeload.github.com/${options.template.repository}/tar.gz${ options.template.ref ? `/${options.template.ref}` : "" }`, ); } logger.info("Downloading template..."); await pipeline(httpStream, createWriteStream(tmpFile)); let dirToExtract = options.template.path; if (options.template.provider === "github") { dirToExtract = `${options.template.repository.split("/").pop()}-${( options.template.ref ?? "main" ).replaceAll(/\//g, "-")}/${ options.template.path ? normalize(options.template.path) : "" }`; // For some reason, GitHub strips the `v` prefix of tagged refs in the Compas repo. // Have to figure out a better way to fix this. When resolving the tree // `compasjs/compas/tree/v0.0.211` this isn't happening. if (options.template.ref && /^v\d+\.\d+\.\d+$/.test(options.template.ref)) { dirToExtract = dirToExtract.replace( options.template.ref, options.template.ref.substring(1), ); } } logger.info(`Extracting template...`); await tar.extract( { file: tmpFile, cwd: options.outputDirectory, strip: options.template.path ? normalize(options.template.path).split("/").length + 1 : 1, }, [dirToExtract], ); } /** * @param {string} url * @returns {Promise<NodeJS.ReadableStream>} */ export function templateGetHttpStream(url) { return new Promise((resolve, reject) => { https .get(url, (response) => { const code = response.statusCode ?? 200; if (code >= 400) { reject( new AppError("fetch.error", code, { message: response.statusMessage, }), ); } else if (code >= 300) { // @ts-expect-error templateGetHttpStream(response.headers.location).then( resolve, reject, ); } else { resolve(response); } }) .on("error", reject); }); } /** * Do necessary post-processing. * * @param {import("@compas/stdlib").Logger} logger * @param {import("./arg-parser.js").CreateCompasArgs} options * @param {string} compasVersion * @returns {Promise<void>} */ export async function templatePostProcess(logger, options, compasVersion) { logger.info("Post processing..."); const packageJson = JSON.parse( await readFile(pathJoin(options.outputDirectory, "package.json"), "utf-8"), ); const metadata = packageJson.exampleMetadata; delete packageJson.name; delete packageJson.exampleMetadata; for (const depType of ["dependencies", "devDependencies"]) { for (const key of Object.keys(packageJson[depType] ?? {})) { if (key === "compas" || key.startsWith("@compas/")) { if (packageJson[depType][key] === "*") { packageJson[depType][key] = compasVersion; } } } } await writeFile( pathJoin(options.outputDirectory, "package.json"), JSON.stringify(packageJson, null, 2), ); logger.info("Running npm/yarn..."); let command = `npm`; if (environment.npm_config_user_agent?.startsWith("yarn")) { command = "yarn"; } await spawn(command, command === "npm" ? ["i"] : [], { cwd: options.outputDirectory, }); if (metadata?.generating) { logger.info("Generating..."); await exec(`npx ${metadata.generating}`, { cwd: options.outputDirectory, }); } logger.info("Collecting environment info..."); await spawn(`npx`, ["compas", "check-env"], { cwd: options.outputDirectory, }); logger.info( `Started a new Compas project in ${options.outputDirectory}. See the README.md for how to get started.`, ); }