Path: blob/main/src/command/create/artifacts/project.ts
3587 views
/*1* project.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import {7ArtifactCreator,8CreateContext,9CreateDirective,10} from "../cmd-types.ts";1112import { capitalizeTitle } from "../../../core/text.ts";13import { kMarkdownEngine } from "../../../execute/types.ts";14import { projectCreate } from "../../../project/project-create.ts";15import {16parseProjectType,17projectType,18projectTypeAliases,19projectTypes,20} from "../../../project/types/project-types.ts";2122import { Input, Select } from "cliffy/prompt/mod.ts";23import { join } from "../../../deno_ral/path.ts";2425// ensures project types are registered26import "../../../project/types/register.ts";27import { warning } from "../../../deno_ral/log.ts";2829const kProjectTypes = projectTypes();30const kProjectTypeAliases = projectTypeAliases();31const kProjectTypesAndAliases = [32...kProjectTypes,33...kProjectTypeAliases,34];3536const kType = "type";37const kSubdirectory = "subdirectory";3839const kBlogTypeAlias = "blog";40const kConfluenceAlias = "confluence";4142const kTypeProj = "project";4344const kProjectCreateTypes = [45...kProjectTypes,46kBlogTypeAlias,47kConfluenceAlias,48];49const kProjectTypeOrder = [50"default",51"website",52kBlogTypeAlias,53"manuscript",54"book",55kConfluenceAlias,56];5758export const projectArtifactCreator: ArtifactCreator = {59displayName: "Project",60type: kTypeProj,61resolveOptions,62finalizeOptions,63nextPrompt,64createArtifact,65};6667function resolveOptions(args: string[]): Record<string, unknown> {68// The first argument is the type (website, default, etc...)69// The second argument is the directory70const typeRaw = args.length > 0 ? args[0] : undefined;71const directoryRaw = args.length > 1 ? args[1] : undefined;72const titleRaw = args.length > 2 ? args[2] : undefined;7374const options: Record<string, unknown> = {};75if (typeRaw) {76if (kProjectCreateTypes.includes(typeRaw)) {77// This is a recognized type78options[kType] = typeRaw;79}80}81// Populate a directory, if provided82if (directoryRaw) {83options[kSubdirectory] = directoryRaw;84}8586if (titleRaw) {87options.name = titleRaw;88}8990return options;91}9293// We specially handle website and blog94// (website means a website with the default template,95// blog means a website with the blog template)96function resolveTemplate(type: string) {97if (type === "website") {98return {99type,100template: "default",101};102} else if (type === kBlogTypeAlias) {103return {104type: "website",105template: "blog",106};107} else if (type === "confluence") {108return {109type: "default",110template: kConfluenceAlias,111};112} else {113return {114type,115};116}117}118119function finalizeOptions(createContext: CreateContext) {120const typeStr = createContext.options[kType] as string || "default";121// Resolve the type and template122const resolved = resolveTemplate(typeStr);123const subdirectory = createContext.options[kSubdirectory] as string;124if (!subdirectory) {125throw new Error(126"A directory is required for project creation with \`quarto create project\`",127);128}129const directory = join(createContext.cwd, subdirectory);130let name = createContext.options.name;131if (!name) {132name = defaultName(subdirectory, typeStr);133warning(134`No 'title' for project provided in \`quarto create project\`. Using '${name}' as default.`,135);136}137const template = resolved.template138? `${resolved.type}:${resolved.template}`139: resolved.type;140141return {142displayType: "project",143name,144directory,145template,146} as CreateDirective;147}148149function nextPrompt(150createOptions: CreateContext,151) {152// First ensure that there is a type153if (!createOptions.options[kType]) {154const orderedTypes = kProjectCreateTypes.sort((t1, t2) => {155if (t1 === t2) {156return 0;157} else if (kProjectTypeOrder.indexOf(t1) === -1) {158return 1;159} else {160return kProjectTypeOrder.indexOf(t1) - kProjectTypeOrder.indexOf(t2);161}162});163164return {165name: kType,166message: "Type",167type: Select,168options: orderedTypes.map((t) => {169return {170name: t,171value: t,172};173}),174};175}176177// Collect a name178if (!createOptions.options[kSubdirectory]) {179return {180name: kSubdirectory,181message: "Directory",182type: Input,183};184}185186if (!createOptions.options.name) {187return {188name: "name",189message: "Title",190type: Input,191default: defaultName(192createOptions.options[kSubdirectory] as string,193createOptions.options[kType] as string,194),195};196}197}198199async function createArtifact(200createDirective: CreateDirective,201quiet?: boolean,202) {203const dir = createDirective.directory;204const projectTitle = createDirective.name;205const directiveType = createDirective.template;206207// Parse the project type and template208const { type, template } = parseProjectType(directiveType);209210// Validate the type211if (kProjectTypesAndAliases.indexOf(type) === -1) {212throw new Error(213`Project type must be one of ${214kProjectTypes.join(", ")215}, but got "${type}".`,216);217}218219// Validate the template220const projType = projectType(type);221if (template && !projType.templates?.includes(template)) {222if (projType.templates) {223throw new Error(224`Project template must be one of ${225projType.templates.join(", ")226}, but got "${template}".`,227);228} else {229throw new Error(230`The project type ${type} does not support any templates.`,231);232}233}234235await projectCreate({236dir,237type: type,238title: projectTitle,239scaffold: true,240engine: kMarkdownEngine,241template: template,242quiet,243});244245return {246path: dir,247openfiles: type !== "default"248? ["index.qmd", "_quarto.yml"]249: ["_quarto.yml"],250};251}252253// choose a default name if none provided in the createContext254function defaultName(subdirectory: string, type: string) {255return subdirectory !== "." ? subdirectory : type;256}257258259