Path: blob/main/src/command/create/artifacts/extension.ts
6446 views
/*1* extension.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import {7ArtifactCreator,8CreateContext,9CreateDirective,10} from "../cmd-types.ts";1112import { ejsData, renderAndCopyArtifacts } from "./artifact-shared.ts";1314import { resourcePath } from "../../../core/resources.ts";1516import { Input, Select } from "cliffy/prompt/mod.ts";17import { dirname, join } from "../../../deno_ral/path.ts";18import { copySync, ensureDirSync, existsSync } from "../../../deno_ral/fs.ts";1920const kType = "type";21const kSubType = "subtype";22const kName = "name";23const kCellLanguage = "cellLanguage";2425const kTypeExtension = "extension";2627interface ExtensionType {28name: string;29value: string;30openfiles: string[];31}3233const kExtensionTypes: Array<string | ExtensionType> = [34{ name: "shortcode", value: "shortcode", openfiles: ["example.qmd"] },35{ name: "filter", value: "filter", openfiles: ["example.qmd"] },36{37name: "revealjs plugin",38value: "revealjs-plugin",39openfiles: ["example.qmd"],40},41{ name: "journal format", value: "journal", openfiles: ["template.qmd"] },42{ name: "custom format", value: "format", openfiles: ["template.qmd"] },43{ name: "metadata", value: "metadata", openfiles: [] },44{ name: "brand", value: "brand", openfiles: [] },45{ name: "engine", value: "engine", openfiles: ["example.qmd"] },46];4748const kExtensionSubtypes: Record<string, string[]> = {49"format": ["html", "pdf", "docx", "revealjs", "typst"],50};5152const kExtensionValues = kExtensionTypes.filter((t) => typeof t === "object")53.map((t) => (t as { name: string; value: string }).value);5455export const extensionArtifactCreator: ArtifactCreator = {56displayName: "Extension",57type: kTypeExtension,58resolveOptions,59finalizeOptions,60nextPrompt,61createArtifact,62};6364function resolveOptions(args: string[]): Record<string, unknown> {65// The first argument is the extension type66// The second argument is the name67// The third argument is the cell language (for engine extensions)68const typeRaw = args.length > 0 ? args[0] : undefined;69const nameRaw = args.length > 1 ? args[1] : undefined;70const cellLanguageRaw = args.length > 2 ? args[2] : undefined;7172const options: Record<string, unknown> = {};7374// Populate the type data75if (typeRaw) {76const [type, template] = typeRaw.split(":");77options[kType] = type;78options[kSubType] = template;79}8081// Populate a directory, if provided82if (nameRaw) {83options[kName] = nameRaw;84}8586// For engine type, populate the cell language if provided87if (cellLanguageRaw && options[kType] === "engine") {88options[kCellLanguage] = cellLanguageRaw;89}9091return options;92}9394function finalizeOptions(createOptions: CreateContext) {95// There should be a name96if (!createOptions.options.name) {97throw new Error("Required property 'name' is missing.");98}99100// Is the type valid101const type = createOptions.options[kType] as string;102if (!kExtensionValues.includes(type)) {103throw new Error(104`The type ${type} isn't valid. Expected one of ${105kExtensionValues.join(", ")106}`,107);108}109110// Is the subtype valid111const subType = createOptions.options[kSubType] as string;112const subTypes = kExtensionSubtypes[type];113if (subTypes && !subTypes.includes(subType)) {114throw new Error(115`The sub type ${subType} isn't valid. Expected one of ${116subTypes.join(", ")117}`,118);119}120121// Form a template122const template = createOptions.options[kSubType]123? `${createOptions.options[kType]}:${createOptions.options[kSubType]}`124: createOptions.options[kType];125126// Provide a directory and title127return {128displayType: "extension",129name: createOptions.options[kName],130directory: join(131createOptions.cwd,132createOptions.options[kName] as string,133),134template,135options: createOptions.options,136} as CreateDirective;137}138139function nextPrompt(140createOptions: CreateContext,141) {142// First ensure that there is a type143if (!createOptions.options[kType]) {144return {145name: kType,146message: "Type",147type: Select,148options: kExtensionTypes.map((t) => {149if (t === "---") {150return Select.separator("--------");151} else {152return t;153}154}),155};156}157158const subTypes = kExtensionSubtypes[createOptions.options[kType] as string];159if (160!createOptions.options[kSubType] &&161subTypes && subTypes.length > 0162) {163return {164name: kSubType,165message: "Base Format",166type: Select,167options: subTypes.map((t) => {168return {169name: t,170value: t,171};172}),173};174}175176// Collect a title177if (!createOptions.options[kName]) {178return {179name: kName,180message: "Extension Name",181type: Input,182};183}184185// Collect cell language for engine extensions186if (187createOptions.options[kType] === "engine" &&188!createOptions.options[kCellLanguage]189) {190return {191name: kCellLanguage,192message: "Default cell language name",193type: Input,194default: createOptions.options[kName] as string,195};196}197}198199function typeFromTemplate(template: string) {200return template.split(":")[0];201}202203async function createArtifact(204createDirective: CreateDirective,205quiet?: boolean,206) {207// Find the type using the template208const createType = typeFromTemplate(createDirective.template);209const extType = kExtensionTypes.find((type) => {210if (typeof type === "object") {211return type.value === createType;212} else {213return false;214}215});216if (!extType) {217throw new Error(`Unrecognized extension type ${createType}`);218}219const openfiles = extType ? (extType as ExtensionType).openfiles : [];220221// Create the extension222await createExtension(createDirective, quiet);223return {224path: createDirective.directory,225openfiles,226};227}228229async function createExtension(230createDirective: CreateDirective,231quiet?: boolean,232) {233// The folder for this extension234const artifact = templateFolder(createDirective);235236// The target directory237const target = createDirective.directory;238239if (existsSync(target)) {240// The target directory already exists241throw new Error(242`The directory ${target} already exists. Quarto extensions must have unique names - please modify the existing extension or use a unique name.`,243);244}245246// Data for this extension247const data = await ejsData(createDirective);248249// Render or copy the artifact250const filesCreated = renderAndCopyArtifacts(251target,252artifact,253createDirective,254data,255quiet,256);257258return filesCreated[0];259}260261function templateFolder(createDirective: CreateDirective) {262const basePath = resourcePath(join("create", "extensions"));263const artifactFolderName = createDirective.template.replace(":", "-");264return join(basePath, artifactFolderName);265}266267268